From 4442b7507846cb4653ebea39a49c0e23918b2d5b Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Tue, 4 Aug 2026 23:58:26 +0900 Subject: [PATCH 1/7] Editor: Keep the canvas height stable while resizing the canvas Dragging the canvas resize handles left the inline height of `.editor-resizable-editor` as a percentage of its container, such as `104.632%`, and collapsed the white page to its content height. `re-resizable` renders from its own state while resizing and ignores the `size` prop, converting the pixel height it started with into a percentage whenever that prop is a percentage. Switching the height to `100%` on resize start therefore never applied, it only changed the unit of the stale value. Hold the device height the drag started at until the drag ends, so the prop stays in pixels for the whole drag. The iframe body's `min-height: 100vh` was keyed off the device height, which stops applying as soon as the width leaves the preset, so the body collapsed on the first move and again on release. Key it off the canvas having a constrained width instead. Co-Authored-By: Claude --- .../src/components/visual-editor/index.js | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/editor/src/components/visual-editor/index.js b/packages/editor/src/components/visual-editor/index.js index a7ea853801c759..2b2f991cc82b62 100644 --- a/packages/editor/src/components/visual-editor/index.js +++ b/packages/editor/src/components/visual-editor/index.js @@ -192,6 +192,7 @@ function VisualEditor( { const localRef = useRef(); const [ isResizingCanvas, setIsResizingCanvas ] = useState( false ); + const [ resizingCanvasHeight, setResizingCanvasHeight ] = useState(); const [ globalLayoutSettings ] = useSettings( 'layout' ); // fallbackLayout is used if there is no Post Content, @@ -347,13 +348,14 @@ function VisualEditor( { ); const centerContentCSS = `display:flex;align-items:center;justify-content:center;`; - const shouldExpandIframeBody = - canvasHeight !== undefined && - ! isResizablePostType && - ! isResizingCanvas; - const iframeBodyMinHeightCSS = shouldExpandIframeBody - ? 'min-height:100vh;' - : ''; + // A dragged width drops the device height, so hold the starting height + // until the drag ends. + const effectiveCanvasHeight = isResizingCanvas + ? resizingCanvasHeight + : canvasHeight; + // Fill the frame at any constrained width, not just device presets. + const iframeBodyMinHeightCSS = + hasCanvasWidth && ! isResizablePostType ? 'min-height:100vh;' : ''; const iframeStyles = useMemo( () => { return [ @@ -433,11 +435,14 @@ function VisualEditor( { enableResizing && canvasWidth ? canvasWidth + 'px' : '100%' } height={ - enableResizing && canvasHeight && ! isResizingCanvas - ? canvasHeight + 'px' + enableResizing && effectiveCanvasHeight + ? effectiveCanvasHeight + 'px' : '100%' } - onResizeStart={ () => setIsResizingCanvas( true ) } + onResizeStart={ () => { + setResizingCanvasHeight( canvasHeight ); + setIsResizingCanvas( true ); + } } onResizeStop={ () => setIsResizingCanvas( false ) } > Date: Wed, 5 Aug 2026 00:08:02 +0900 Subject: [PATCH 2/7] Editor: Drop redundant comments in the visual editor canvas sizing The two comments restated what the adjacent expressions already say, so they only added noise to the resize handling logic. Co-Authored-By: Claude --- packages/editor/src/components/visual-editor/index.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/editor/src/components/visual-editor/index.js b/packages/editor/src/components/visual-editor/index.js index 2b2f991cc82b62..c7a2cb47e551a3 100644 --- a/packages/editor/src/components/visual-editor/index.js +++ b/packages/editor/src/components/visual-editor/index.js @@ -348,12 +348,9 @@ function VisualEditor( { ); const centerContentCSS = `display:flex;align-items:center;justify-content:center;`; - // A dragged width drops the device height, so hold the starting height - // until the drag ends. const effectiveCanvasHeight = isResizingCanvas ? resizingCanvasHeight : canvasHeight; - // Fill the frame at any constrained width, not just device presets. const iframeBodyMinHeightCSS = hasCanvasWidth && ! isResizablePostType ? 'min-height:100vh;' : ''; From 44fb5e4618bb4b3725447e332d484a655ae0af3b Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Wed, 5 Aug 2026 17:38:53 +0900 Subject: [PATCH 3/7] Editor: Always fill the editor height instead of a device aspect ratio The canvas frame was given a fixed pixel height derived from the device aspect ratio at the mobile and tablet preset widths, so the frame stopped short of the editor height and had to be held stable while resizing. Drop the ratio entirely and let the canvas fill the available height at every width, which also removes the now unused getCanvasHeight selector. While dragging, clearing the canvas width near the container edge turned resizing off mid-drag, unmounting the handle under the pointer along with the canvas frame styles. Only snap back to a fluid width once the drag has ended. Co-Authored-By: Claude --- .../src/components/resizable-editor/index.js | 6 +- .../src/components/visual-editor/index.js | 21 +----- .../editor/src/store/private-selectors.js | 53 -------------- .../src/store/test/private-selectors.js | 71 ------------------- 4 files changed, 5 insertions(+), 146 deletions(-) diff --git a/packages/editor/src/components/resizable-editor/index.js b/packages/editor/src/components/resizable-editor/index.js index bef6e97ff935e4..8066aaf10eddc9 100644 --- a/packages/editor/src/components/resizable-editor/index.js +++ b/packages/editor/src/components/resizable-editor/index.js @@ -74,11 +74,11 @@ function ResizableEditor( { ); const updateCanvasWidth = useCallback( - ( element ) => { + ( element, isResizeEnd ) => { const currentWidth = element.offsetWidth; const containerWidth = element.parentElement?.offsetWidth ?? 0; setCanvasWidth( - isAtMaxWidth( currentWidth, containerWidth, 80 ) + isResizeEnd && isAtMaxWidth( currentWidth, containerWidth, 80 ) ? undefined : currentWidth ); @@ -107,7 +107,7 @@ function ResizableEditor( { updateCanvasWidth( element ); } } onResizeStop={ ( event, direction, element ) => { - updateCanvasWidth( element ); + updateCanvasWidth( element, true ); setIsResizing( false ); onResizeStop?.(); } } diff --git a/packages/editor/src/components/visual-editor/index.js b/packages/editor/src/components/visual-editor/index.js index c7a2cb47e551a3..ddf8323a067ddc 100644 --- a/packages/editor/src/components/visual-editor/index.js +++ b/packages/editor/src/components/visual-editor/index.js @@ -14,7 +14,7 @@ import { RecursionProvider, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; -import { useEffect, useRef, useMemo, useState } from '@wordpress/element'; +import { useEffect, useRef, useMemo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { parse } from '@wordpress/blocks'; import { store as coreStore } from '@wordpress/core-data'; @@ -111,7 +111,6 @@ function VisualEditor( { styles, hasCanvasWidth, canvasWidth, - canvasHeight, } = useSelect( ( select ) => { const { getCurrentPostId, @@ -121,7 +120,6 @@ function VisualEditor( { getRenderingMode, getDeviceType, getCanvasWidth, - getCanvasHeight, } = unlock( select( editorStore ) ); const { getPostType, getEditedEntityRecord } = select( coreStore ); const postTypeSlug = getCurrentPostType(); @@ -166,7 +164,6 @@ function VisualEditor( { styles: editorSettings.styles, hasCanvasWidth: _canvasWidth !== undefined, canvasWidth: _canvasWidth, - canvasHeight: getCanvasHeight(), }; }, [] ); const { isCleanNewPost } = useSelect( editorStore ); @@ -191,8 +188,6 @@ function VisualEditor( { }, [] ); const localRef = useRef(); - const [ isResizingCanvas, setIsResizingCanvas ] = useState( false ); - const [ resizingCanvasHeight, setResizingCanvasHeight ] = useState(); const [ globalLayoutSettings ] = useSettings( 'layout' ); // fallbackLayout is used if there is no Post Content, @@ -348,9 +343,6 @@ function VisualEditor( { ); const centerContentCSS = `display:flex;align-items:center;justify-content:center;`; - const effectiveCanvasHeight = isResizingCanvas - ? resizingCanvasHeight - : canvasHeight; const iframeBodyMinHeightCSS = hasCanvasWidth && ! isResizablePostType ? 'min-height:100vh;' : ''; @@ -431,16 +423,7 @@ function VisualEditor( { width={ enableResizing && canvasWidth ? canvasWidth + 'px' : '100%' } - height={ - enableResizing && effectiveCanvasHeight - ? effectiveCanvasHeight + 'px' - : '100%' - } - onResizeStart={ () => { - setResizingCanvasHeight( canvasHeight ); - setIsResizingCanvas( true ); - } } - onResizeStop={ () => setIsResizingCanvas( false ) } + height="100%" > ( state ) => { - const blockEditorSelect = unlock( select( blockEditorStore ) ); - if ( blockEditorSelect.isZoomOut() ) { - return undefined; - } - const canvasWidth = state.canvasWidth; - const viewportSettings = - blockEditorSelect.getSettings().__experimentalFeatures?.viewport; - const deviceType = getDeviceTypeByCanvasWidth( - canvasWidth, - viewportSettings - ); - // Only apply the device height at the preset width; a dragged width - // within a band frees the canvas to fill the editor. - if ( - canvasWidth !== - getCanvasWidthByDeviceType( deviceType, viewportSettings ) - ) { - return undefined; - } - const ratio = DEVICE_ASPECT_RATIO_BY_DEVICE_TYPE[ deviceType ]; - if ( ratio && canvasWidth > 0 ) { - return Math.round( canvasWidth * ratio ); - } - return undefined; - } -); - /** * Returns the current revisions page number. * diff --git a/packages/editor/src/store/test/private-selectors.js b/packages/editor/src/store/test/private-selectors.js index ac9df38ab78f4d..59c9560f1c5c1f 100644 --- a/packages/editor/src/store/test/private-selectors.js +++ b/packages/editor/src/store/test/private-selectors.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -9,7 +8,6 @@ import { store as preferencesStore } from '@wordpress/preferences'; * Internal dependencies */ import { - getCanvasHeight, getDefaultRenderingMode, getPostBlocksByName, isCollaborationEnabledForCurrentPost, @@ -253,72 +251,3 @@ describe( 'getDefaultRenderingMode', () => { } ); } ); } ); - -describe( 'getCanvasHeight', () => { - // Default viewport breakpoints: Mobile 480, Tablet 782. - function setupRegistry( { isZoomOut = false, viewport } = {} ) { - getCanvasHeight.registry = { - select: ( store ) => { - if ( store === blockEditorStore ) { - // In a real registry, `unlock( select( blockEditorStore ) )` - // returns the combined public + private selectors, so both - // getSettings and isZoomOut live in the unlocked map. - const selectors = {}; - lock( selectors, { - getSettings: () => ( { - __experimentalFeatures: viewport - ? { viewport } - : {}, - } ), - isZoomOut: () => isZoomOut, - } ); - return selectors; - } - }, - }; - } - - it( 'returns the portrait height at the mobile preset width', () => { - // Mobile aspect ratio is 8/5 (portrait): 480 * 8/5 = 768. - setupRegistry(); - expect( getCanvasHeight( { canvasWidth: 480 } ) ).toBe( 768 ); - } ); - - it( 'returns the portrait height at the tablet preset width', () => { - // Tablet aspect ratio is 4/3 (portrait): 782 * 4/3 = 1043. - setupRegistry(); - expect( getCanvasHeight( { canvasWidth: 782 } ) ).toBe( 1043 ); - } ); - - it( 'returns undefined for desktop (no aspect ratio applies)', () => { - setupRegistry(); - expect( getCanvasHeight( { canvasWidth: 1200 } ) ).toBeUndefined(); - } ); - - it( 'returns undefined when zoom-out is active', () => { - setupRegistry( { isZoomOut: true } ); - expect( getCanvasHeight( { canvasWidth: 480 } ) ).toBeUndefined(); - } ); - - it( 'returns undefined when the width is dragged within a device band but is not the preset', () => { - // 400 resolves to Mobile (at or below the 480 breakpoint) but is not the - // 480 preset, so the device height does not apply and the canvas fills. - setupRegistry(); - expect( getCanvasHeight( { canvasWidth: 400 } ) ).toBeUndefined(); - } ); - - it( 'returns undefined when canvasWidth is not set', () => { - setupRegistry(); - expect( getCanvasHeight( {} ) ).toBeUndefined(); - } ); - - it( 'uses custom viewport breakpoints when provided', () => { - // Custom mobile preset 640: 640 * 8/5 = 1024. - setupRegistry( { - viewport: { mobile: '640px', tablet: '1024px' }, - } ); - expect( getCanvasHeight( { canvasWidth: 640 } ) ).toBe( 1024 ); - // Custom tablet preset 1024: 1024 * 4/3 = 1365 (rounded). - expect( getCanvasHeight( { canvasWidth: 1024 } ) ).toBe( 1365 ); - } ); -} ); From 9670e9797cf63fee879015f74e04f1cdc89e0300 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Fri, 7 Aug 2026 19:15:07 +0900 Subject: [PATCH 4/7] Editor: Scale the canvas height with its width instead of always filling A width-constrained canvas (a device preview or a dragged resize) kept the full editor height, which reads as an unnaturally elongated frame on tall screens. Derive the height from the canvas width by interpolating between the aspect ratio of the available space and a portrait target ratio, mirroring the site editor's resizable frame, and clamp it so it never exceeds the space available. Using the container width as the upper anchor keeps the height monotonic in the width and exactly 100% at full width. `re-resizable` ignores the `size` prop while a drag is in progress, so the height is animated through motion to stay in sync with the width during the drag. The CSS height transition is dropped to avoid animating it twice. Co-Authored-By: Claude --- .../src/components/resizable-editor/index.js | 15 ++++- .../components/resizable-editor/style.scss | 3 +- .../src/components/visual-editor/index.js | 65 ++++++++++++++++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/components/resizable-editor/index.js b/packages/editor/src/components/resizable-editor/index.js index 8066aaf10eddc9..f8474f74678da5 100644 --- a/packages/editor/src/components/resizable-editor/index.js +++ b/packages/editor/src/components/resizable-editor/index.js @@ -8,7 +8,11 @@ import clsx from 'clsx'; */ import { useDispatch } from '@wordpress/data'; import { useRef, useCallback, useState } from '@wordpress/element'; -import { ResizableBox } from '@wordpress/components'; +import { + ResizableBox, + __unstableMotion as motion, +} from '@wordpress/components'; +import { useReducedMotion } from '@wordpress/compose'; /** * Internal dependencies @@ -52,6 +56,7 @@ function ResizableEditor( { children, } ) { const [ isResizing, setIsResizing ] = useState( false ); + const disableMotion = useReducedMotion(); const { setCanvasWidth } = unlock( useDispatch( editorStore ) ); const resizableRef = useRef(); @@ -88,6 +93,14 @@ function ResizableEditor( { return ( a + ( b - a ) * amount; + + // The narrower the canvas within the available space, the closer to the + // target aspect ratio. + const lerpFactor = + 1 - + Math.max( + 0, + Math.min( + 1, + ( width - CANVAS_MIN_WIDTH ) / + ( containerSize.width - CANVAS_MIN_WIDTH ) + ) + ); + const aspectRatio = lerp( + containerSize.width / containerSize.height, + CANVAS_TARGET_ASPECT_RATIO, + lerpFactor + ); + + return Math.min( Math.round( width / aspectRatio ), containerSize.height ); +} + function VisualEditor( { // Ideally as we unify post and site editors, we won't need these props. autoFocus, @@ -190,6 +229,16 @@ function VisualEditor( { const localRef = useRef(); const [ globalLayoutSettings ] = useSettings( 'layout' ); + const [ containerSize, setContainerSize ] = useState(); + const containerRef = useResizeObserver( ( entries ) => { + const { width, height } = entries[ 0 ].contentRect; + setContainerSize( ( size ) => + size?.width === width && size?.height === height + ? size + : { width, height } + ); + } ); + // fallbackLayout is used if there is no Post Content, // and for Post Title. const fallbackLayout = useMemo( () => { @@ -342,6 +391,15 @@ function VisualEditor( { ! isPreview && renderingMode === 'post-only' && ! isDesignPostType ); + const shouldConstrainCanvasHeight = + enableResizing && + canvasWidth && + containerSize?.height > 0 && + containerSize?.width > CANVAS_MIN_WIDTH; + const canvasHeight = shouldConstrainCanvasHeight + ? getCanvasHeight( canvasWidth, containerSize ) + : '100%'; + const centerContentCSS = `display:flex;align-items:center;justify-content:center;`; const iframeBodyMinHeightCSS = hasCanvasWidth && ! isResizablePostType ? 'min-height:100vh;' : ''; @@ -402,6 +460,7 @@ function VisualEditor( { return (
Date: Mon, 10 Aug 2026 15:25:23 +0900 Subject: [PATCH 5/7] Editor: Remove unused blockEditorStore import from private selectors test --- packages/editor/src/store/test/private-selectors.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/editor/src/store/test/private-selectors.js b/packages/editor/src/store/test/private-selectors.js index c63220650d9433..f6418accdc157e 100644 --- a/packages/editor/src/store/test/private-selectors.js +++ b/packages/editor/src/store/test/private-selectors.js @@ -1,4 +1,3 @@ -import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; import { From 9e1fafedc2a6a3b8a488a2c96de07327ffff169e Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Mon, 10 Aug 2026 15:31:29 +0900 Subject: [PATCH 6/7] Editor: Remove unused onResizeStart and onResizeStop props from ResizableEditor --- packages/editor/src/components/resizable-editor/index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/editor/src/components/resizable-editor/index.js b/packages/editor/src/components/resizable-editor/index.js index e87d4656d78dfb..eb23f649711da7 100644 --- a/packages/editor/src/components/resizable-editor/index.js +++ b/packages/editor/src/components/resizable-editor/index.js @@ -40,8 +40,6 @@ function ResizableEditor( { enableResizing, width = '100%', height = '100%', - onResizeStart, - onResizeStop, children, } ) { const [ isResizing, setIsResizing ] = useState( false ); @@ -103,7 +101,6 @@ function ResizableEditor( { } } onResizeStart={ () => { setIsResizing( true ); - onResizeStart?.(); } } onResize={ ( event, direction, element ) => { updateCanvasWidth( element ); @@ -111,7 +108,6 @@ function ResizableEditor( { onResizeStop={ ( event, direction, element ) => { updateCanvasWidth( element, true ); setIsResizing( false ); - onResizeStop?.(); } } minWidth={ 300 } maxWidth="100%" From 55fd27db728c9e5cfea40607e3ecbc68f5c1fa61 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Mon, 10 Aug 2026 15:40:58 +0900 Subject: [PATCH 7/7] Editor: Add unit tests for the visual editor getCanvasHeight helper --- .../src/components/visual-editor/index.js | 2 +- .../visual-editor/test/get-canvas-height.js | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 packages/editor/src/components/visual-editor/test/get-canvas-height.js diff --git a/packages/editor/src/components/visual-editor/index.js b/packages/editor/src/components/visual-editor/index.js index 9d8677a3d27bc9..7bcefd4dce4753 100644 --- a/packages/editor/src/components/visual-editor/index.js +++ b/packages/editor/src/components/visual-editor/index.js @@ -93,7 +93,7 @@ const CANVAS_TARGET_ASPECT_RATIO = 9 / 16; * @param {Object} containerSize The available space, as `{ width, height }` in pixels. * @return {number} The canvas height in pixels. */ -function getCanvasHeight( width, containerSize ) { +export function getCanvasHeight( width, containerSize ) { const lerp = ( a, b, amount ) => a + ( b - a ) * amount; // The narrower the canvas within the available space, the closer to the diff --git a/packages/editor/src/components/visual-editor/test/get-canvas-height.js b/packages/editor/src/components/visual-editor/test/get-canvas-height.js new file mode 100644 index 00000000000000..07b0f6b1c46b78 --- /dev/null +++ b/packages/editor/src/components/visual-editor/test/get-canvas-height.js @@ -0,0 +1,50 @@ +import { getCanvasHeight } from '../index'; + +describe( 'getCanvasHeight', () => { + it( 'fills the available height at the full container width', () => { + expect( getCanvasHeight( 1000, { width: 1000, height: 800 } ) ).toBe( + 800 + ); + } ); + + it( 'uses the target 9:16 aspect ratio at the minimum canvas width', () => { + // 300 / ( 9 / 16 ) = 533.33, rounded to 533. + expect( getCanvasHeight( 300, { width: 1000, height: 800 } ) ).toBe( + 533 + ); + } ); + + it( 'interpolates between the container and target aspect ratios in between', () => { + // Halfway between 300 and 1000, so halfway between the container ratio + // ( 1.25 ) and the target ratio ( 0.5625 ): 650 / 0.90625 = 717.24. + expect( getCanvasHeight( 650, { width: 1000, height: 800 } ) ).toBe( + 717 + ); + } ); + + it( 'grows with the canvas width', () => { + const heights = [ 300, 500, 700, 900 ].map( ( width ) => + getCanvasHeight( width, { width: 1000, height: 800 } ) + ); + expect( heights ).toEqual( [ 533, 659, 733, 781 ] ); + } ); + + it( 'never exceeds the available height', () => { + // In a short container, the 9:16 height of 533 is clamped to 300. + expect( getCanvasHeight( 300, { width: 1000, height: 300 } ) ).toBe( + 300 + ); + } ); + + it( 'clamps widths outside the resizable range', () => { + // Wider than the container: the container aspect ratio applies, and the + // height is capped at the container height. + expect( getCanvasHeight( 1200, { width: 1000, height: 800 } ) ).toBe( + 800 + ); + // Narrower than the minimum width: the target aspect ratio applies. + expect( getCanvasHeight( 200, { width: 1000, height: 800 } ) ).toBe( + 356 + ); + } ); +} );