diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss
index 596c177eab2f32..d1a5fea4e1a599 100644
--- a/packages/block-editor/src/components/iframe/content.scss
+++ b/packages/block-editor/src/components/iframe/content.scss
@@ -3,55 +3,69 @@
}
.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.
- @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 );
}
-}
-.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;
+ }
}
}
}
diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js
index 76d2e09dfb7a30..aedd1003865e5d 100644
--- a/packages/block-editor/src/components/iframe/index.js
+++ b/packages/block-editor/src/components/iframe/index.js
@@ -30,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 ) {
@@ -225,36 +226,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 ) => {
- 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,6 +239,23 @@ function Iframe( {
containerWidth
);
+ const maxWidth = 750;
+
+ useScaleCanvas( {
+ scale:
+ scale === 'auto-scaled'
+ ? ( Math.min( containerWidth, maxWidth ) -
+ parseInt( frameSize ) * 2 ) /
+ scaleContainerWidth
+ : scale,
+ frameSize: parseInt( frameSize ),
+ iframeDocument,
+ contentHeight,
+ containerWidth,
+ isZoomedOut,
+ scaleContainerWidth,
+ } );
+
const disabledRef = useDisabled( { isDisabled: ! readonly } );
const bodyRef = useMergeRefs( [
useBubbleEvents( iframeDocument ),
@@ -275,10 +263,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
@@ -320,118 +304,6 @@ 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 ) {
- return;
- }
-
- const handleZoomOutAnimationClassname = () => {
- clearTimeout( zoomOutAnimationClassnameRef.current );
-
- iframeDocument.documentElement.classList.add(
- 'zoom-out-animation'
- );
-
- zoomOutAnimationClassnameRef.current = setTimeout( () => {
- iframeDocument.documentElement.classList.remove(
- 'zoom-out-animation'
- );
- }, 400 ); // 400ms should match the animation speed used in components/iframe/content.scss
- };
-
- handleZoomOutAnimationClassname();
- iframeDocument.documentElement.classList.add( 'is-zoomed-out' );
-
- return () => {
- handleZoomOutAnimationClassname();
- iframeDocument.documentElement.classList.remove( 'is-zoomed-out' );
- };
- }, [ iframeDocument, isZoomedOut ] );
-
- // Calculate the scaling and CSS variables for the zoom out canvas
- useEffect( () => {
- if ( ! iframeDocument || ! isZoomedOut ) {
- 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
- // 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 === 'auto-scaled'
- ? ( Math.min( containerWidth, maxWidth ) -
- parseInt( frameSize ) * 2 ) /
- scaleContainerWidth
- : 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',
- 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 () => {
- 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,
- 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;
@@ -511,7 +383,7 @@ function Iframe( {
);
return (
-
+
{ containerResizeListener }
{
+ if (
+ ! iframeDocument ||
+ // HACK: Checking if isZoomedOut differs from prevIsZoomedOut here
+ // instead of the dependency array to appease the linter.
+ ( scale === 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 ) * scale +
+ 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 * ( scale / 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' );
+
+ function onZoomOutTransitionEnd() {
+ // Remove the position fixed for the animation.
+ iframeDocument.documentElement.classList.remove(
+ 'zoom-out-animation'
+ );
+
+ // 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;
+ }
+
+ 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, 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, 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',
+ 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`
+ );
+ iframeDocument.documentElement.style.setProperty(
+ '--wp-block-editor-iframe-zoom-out-inner-height',
+ `${ iframeDocument.documentElement.clientHeight }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'
+ // );
+ };
+ }, [
+ scale,
+ frameSize,
+ iframeDocument,
+ contentHeight,
+ containerWidth,
+ scaleContainerWidth,
+ ] );
+}
diff --git a/test/e2e/specs/site-editor/zoom-out.spec.js b/test/e2e/specs/site-editor/zoom-out.spec.js
index 464bd4a4a4efad..e698a94b7cf0dc 100644
--- a/test/e2e/specs/site-editor/zoom-out.spec.js
+++ b/test/e2e/specs/site-editor/zoom-out.spec.js
@@ -3,6 +3,63 @@
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+const EDITOR_ZOOM_OUT_CONTENT = `
+
+
+
First Section Start
+
+
+
+
First Section Center
+
+
+
+
First Section End
+
+
+
+
+
+
Second Section Start
+
+
+
+
Second Section Center
+
+
+
+
Second Section End
+
+
+
+
+
+
Third Section Start
+
+
+
+
Third Section Center
+
+
+
+
Third Section End
+
+
+
+
+
+
Fourth Section Start
+
+
+
+
Fourth Section Center
+
+
+
+
Fourth Section End
+
+`;
+
test.describe( 'Zoom Out', () => {
test.beforeAll( async ( { requestUtils } ) => {
await requestUtils.activateTheme( 'twentytwentyfour' );
@@ -47,4 +104,115 @@ test.describe( 'Zoom Out', () => {
expect( htmlRect.y + paddingTop ).toBeGreaterThan( iframeRect.y );
expect( htmlRect.x ).toBeGreaterThan( iframeRect.x );
} );
+
+ test( 'Toggling zoom state should keep content centered', async ( {
+ 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 } =
+ document.activeElement?.contentDocument ?? document;
+ window.scrollContainer =
+ window.wp.dom.getScrollContainer( activeElement );
+ return window.scrollContainer;
+ } );
+
+ // Test: Test from top of page (scrollTop 0)
+ // Enter Zoom Out
+ await page.getByRole( 'button', { name: 'Zoom Out' } ).click();
+
+ const scrollTopZoomed = await page.evaluate( () => {
+ return window.scrollContainer.scrollTop;
+ } );
+
+ expect( scrollTopZoomed ).toBe( 0 );
+
+ // Exit Zoom Out
+ await page.getByRole( 'button', { name: 'Zoom Out' } ).click();
+
+ const scrollTopNoZoom = await page.evaluate( () => {
+ return window.scrollContainer.scrollTop;
+ } );
+
+ expect( scrollTopNoZoom ).toBe( 0 );
+
+ // Test: Should center the scroll position when zooming out/in
+ const firstSectionEnd = editor.canvas.locator(
+ 'text=First Section End'
+ );
+ const secondSectionStart = editor.canvas.locator(
+ 'text=Second Section Start'
+ );
+ const secondSectionCenter = editor.canvas.locator(
+ 'text=Second Section Center'
+ );
+ const secondSectionEnd = editor.canvas.locator(
+ 'text=Second Section End'
+ );
+ const thirdSectionStart = editor.canvas.locator(
+ 'text=Third Section Start'
+ );
+ const thirdSectionCenter = editor.canvas.locator(
+ 'text=Third Section Center'
+ );
+ const thirdSectionEnd = editor.canvas.locator(
+ 'text=Third Section End'
+ );
+ const fourthSectionStart = editor.canvas.locator(
+ 'text=Fourth Section Start'
+ );
+
+ // Test for second section
+ // Playwright scrolls it to the center of the viewport, so this is what we scroll to.
+ await secondSectionCenter.scrollIntoViewIfNeeded();
+
+ // Because the text is spread with a group height of 100vh, they should both be visible.
+ await expect( firstSectionEnd ).not.toBeInViewport();
+ await expect( secondSectionStart ).toBeInViewport();
+ await expect( secondSectionEnd ).toBeInViewport();
+ await expect( thirdSectionStart ).not.toBeInViewport();
+
+ // After zooming, if we zoomed out with the correct central point, they should both still be visible when toggling zoom out state
+ // Enter Zoom Out
+ await page.getByRole( 'button', { name: 'Zoom Out' } ).click();
+ await expect( firstSectionEnd ).toBeInViewport();
+ await expect( secondSectionStart ).toBeInViewport();
+ await expect( secondSectionEnd ).toBeInViewport();
+ await expect( thirdSectionStart ).toBeInViewport();
+
+ // Exit Zoom Out
+ await page.getByRole( 'button', { name: 'Zoom Out' } ).click();
+ await expect( firstSectionEnd ).not.toBeInViewport();
+ await expect( secondSectionStart ).toBeInViewport();
+ await expect( secondSectionEnd ).toBeInViewport();
+ await expect( thirdSectionStart ).not.toBeInViewport();
+
+ // Test for third section
+ // Playwright scrolls it to the center of the viewport, so this is what we scroll to.
+ await thirdSectionCenter.scrollIntoViewIfNeeded();
+
+ // Because the text is spread with a group height of 100vh, they should both be visible.
+ await expect( secondSectionEnd ).not.toBeInViewport();
+ await expect( thirdSectionStart ).toBeInViewport();
+ await expect( thirdSectionEnd ).toBeInViewport();
+ await expect( fourthSectionStart ).not.toBeInViewport();
+
+ // After zooming, if we zoomed out with the correct central point, they should both still be visible when toggling zoom out state
+ // Enter Zoom Out
+ await page.getByRole( 'button', { name: 'Zoom Out' } ).click();
+ await expect( secondSectionEnd ).toBeInViewport();
+ await expect( thirdSectionStart ).toBeInViewport();
+ await expect( thirdSectionEnd ).toBeInViewport();
+ await expect( fourthSectionStart ).toBeInViewport();
+
+ // Exit Zoom Out
+ await page.getByRole( 'button', { name: 'Zoom Out' } ).click();
+ await expect( secondSectionEnd ).not.toBeInViewport();
+ await expect( thirdSectionStart ).toBeInViewport();
+ await expect( thirdSectionEnd ).toBeInViewport();
+ await expect( fourthSectionStart ).not.toBeInViewport();
+ } );
} );