Fix: Custom HTML block preview keeps expanding when iframe uses height:100vh#78677
Conversation
ciampo
left a comment
There was a problem hiding this comment.
Thank you for working on this!
I think the explanation of the root cause and the fix are correct. Before merging, we need:
- a CHANGELOG entry
- ideally, some unit tests. We could extract the regex into a
VIEWPORT_UNIT_VALUE_REGEXconstant or (if that's too complicated because of the fact thatobserveAndResizeJSis inline in a script) test the<SandBox html="<div style='height:100vh'>x</div>" />component directly (alongside other meaningful tests)
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @KeniVinh. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Hi @ciampo, Thanks for the review. I have added the changelog entry. For tests, I tried as you shared but not sure what to test it exactly. I am bit confused there, hence tests are not added in the PR. |
There was a problem hiding this comment.
This is what we could do in terms of adding unit tests
diff --git a/packages/components/src/sandbox/index.tsx b/packages/components/src/sandbox/index.tsx
--- a/packages/components/src/sandbox/index.tsx
+++ b/packages/components/src/sandbox/index.tsx
@@ -16,6 +16,24 @@ import type { SandBoxProps } from './types';
type SandBoxContentProps = Omit< SandBoxProps, 'allowSameOrigin' >;
+/**
+ * Matches CSS viewport-relative length values such as `100vh`, `50.5vw`,
+ * and `.5dvh`. Used to strip viewport units from user-supplied HTML inside
+ * the sandbox iframe, because those units are relative to the iframe's
+ * own size and would create a measurement feedback loop with the
+ * resize observer.
+ *
+ * Exported for tests. NOTE: an identical regex literal is duplicated
+ * inside `observeAndResizeJS` below because that function is serialized
+ * via `.toString()` and embedded into the iframe's `srcdoc` — it has no
+ * access to this module's scope at runtime. If you change one, change
+ * the other; the "is embedded in the sandbox iframe srcdoc" test
+ * guards against drift.
+ */
+export const VIEWPORT_UNIT_VALUE_REGEX =
+ /^\d*\.?\d+(?:vw|vh|svw|lvw|dvw|svh|lvh|dvh|vi|svi|lvi|dvi|vb|svb|lvb|dvb|vmin|svmin|lvmin|dvmin|vmax|svmax|lvmax|dvmax)$/;
+
const observeAndResizeJS = function () {
const { MutationObserver } = window;
diff --git a/packages/components/src/sandbox/test/index.tsx b/packages/components/src/sandbox/test/index.tsx
--- a/packages/components/src/sandbox/test/index.tsx
+++ b/packages/components/src/sandbox/test/index.tsx
@@ -11,7 +11,7 @@ import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
-import SandBox from '..';
+import SandBox, { VIEWPORT_UNIT_VALUE_REGEX } from '..';
describe( 'SandBox', () => {
const TestWrapper = () => {
@@ -108,4 +108,46 @@ describe( 'SandBox', () => {
expect.stringContaining( 'https://another.super.embed' )
);
} );
+
+ describe( 'VIEWPORT_UNIT_VALUE_REGEX', () => {
+ it.each( [
+ '100vh',
+ '50vw',
+ '0vh',
+ '50.5vh',
+ '.5vh',
+ '100dvh',
+ '50svw',
+ '1lvi',
+ '100vmin',
+ '100vmax',
+ ] )( 'matches viewport unit value %s', ( value ) => {
+ expect( VIEWPORT_UNIT_VALUE_REGEX.test( value ) ).toBe( true );
+ } );
+
+ it.each( [
+ '100px',
+ '50%',
+ '100',
+ 'vh',
+ '.vh',
+ 'calc(100vh - 10px)',
+ '100 vh',
+ '',
+ ] )( 'does not match %s', ( value ) => {
+ expect( VIEWPORT_UNIT_VALUE_REGEX.test( value ) ).toBe( false );
+ } );
+
+ it( 'is embedded in the sandbox iframe srcdoc', () => {
+ // Guards against drift between the exported constant and
+ // the copy inlined into `observeAndResizeJS`, which is
+ // serialized via `.toString()` into the iframe srcdoc and
+ // cannot reference module-scope values at runtime.
+ render( <SandBox html="<p>x</p>" title="Regex Sync Test" /> );
+ const iframe =
+ screen.getByTitle< HTMLIFrameElement >( 'Regex Sync Test' );
+ const srcDoc = iframe.getAttribute( 'srcdoc' ) ?? '';
+
+ expect( srcDoc ).toContain( VIEWPORT_UNIT_VALUE_REGEX.source );
+ } );
+ } );
} );
The duplication is not ideal, but it's the best way we can test the regex in isolation
ciampo
left a comment
There was a problem hiding this comment.
LGTM 🚀
We'll be able to merge once feedback on the CHANGELOG is addressed
e24e72e to
c037033
Compare
|
This issue did not originate with WordPress 7.0, so it may not be necessary to backport it to the 7.0 minor release. |
|
@hbhalodia can you rebase this PR on top of latest trunk and make sure that the CHANGELOG entry is under the latest We'll be able to merge after that |
|
Hi @ciampo, This is now done. PR is now updated with the latest trunk and changelog entry is moved to unreleased section. |
What?
Closes #78539
Why?
How?
Testing Instructions
height:100vh:<iframe src="https://www.photopea.com" style="width:100%; height:100vh; border:0;"></iframe>Testing Instructions for Keyboard
Screenshots or screencast
Before
After
Screen.Recording.2026-05-26.at.3.46.56.PM.mov
Use of AI Tools
Detailed AI review
AI Summary Started
Root cause
The
SandBoxcomponent (used by the Custom HTML block's preview) intentionally strips viewport-unit styles (vh,vw, etc.) from user-supplied HTML, because those units are relative to the iframe's own size and would otherwise create a measurement feedback loop.That stripping is done by
removeViewportStylesinpackages/components/src/sandbox/index.tsxusing this regex:/^\\d+(vw|vh|svw|lvw|dvw|svh|lvh|dvh|vi|svi|lvi|dvi|vb|svb|lvb|dvb|vmin|svmin|lvmin|dvmin|vmax|svmax|lvmax|dvmax)$/The pattern contains a double backslash (
\\d). In a regex literal\\dmeans "a literal backslash followed by the letterd" — not "a digit". So the regex never matches any real CSS value, including100vh, and the strip step is effectively a no-op.Why this causes unbounded expansion
When the user puts
<iframe style="height:100vh">inside the Custom HTML block:100vhis not stripped (because of the broken regex).<iframe>resolves100vhagainst the sandbox iframe's current height.MutationObservermeasuresdocument.body.getBoundingClientRect()and posts the new height to the parent viapostMessage.heightattribute to that value.100vhresolves to a larger value → body grows → observer fires → parent grows the iframe again → infinite loop.That's what produces the "preview keeps expanding, editor becomes heavy, very tall scrollbar" behavior described in the issue.
Fix
Correct the regex (single backslash) and also accept decimal values like
50.5vh:With this in place,
100vhis correctly identified and stripped before the first measurement, the feedback loop is broken, and the preview settles at a stable height.Test steps
<iframe src="https://www.photopea.com" style="width:100%; height:100vh; border:0;"></iframe>Before: the block grows continuously, vertical scrollbar appears, editor becomes sluggish.
After: the preview renders at a stable, finite height with no runaway growth.
AI Summary Completed
Regex101 Test
Previous regex
Link - https://regex101.com/?regex=%5E%5C%5Cd%2B%28vw%7Cvh%7Csvw%7Clvw%7Cdvw%7Csvh%7Clvh%7Cdvh%7Cvi%7Csvi%7Clvi%7Cdvi%7Cvb%7Csvb%7Clvb%7Cdvb%7Cvmin%7Csvmin%7Clvmin%7Cdvmin%7Cvmax%7Csvmax%7Clvmax%7Cdvmax%29%24&testString=100vh%0A&flags=&flavor=pcre2&delimiter=%2F
New Regex
Link - https://regex101.com/?regex=%5E%5Cd%2B%28%3F%3A%5C.%5Cd%2B%29%3F%28vw%7Cvh%7Csvw%7Clvw%7Cdvw%7Csvh%7Clvh%7Cdvh%7Cvi%7Csvi%7Clvi%7Cdvi%7Cvb%7Csvb%7Clvb%7Cdvb%7Cvmin%7Csvmin%7Clvmin%7Cdvmin%7Cvmax%7Csvmax%7Clvmax%7Cdvmax%29%24%0A&testString=100vh%0A&flags=&flavor=pcre2&delimiter=%2F