Skip to content

Bubble editor canvas events to the outer iframe - #85263

Open
jsnajdr wants to merge 7 commits into
trunkfrom
update/iframe-popup-closing
Open

jsnajdr wants to merge 7 commits into
trunkfrom
update/iframe-popup-closing

Conversation

@jsnajdr

@jsnajdr jsnajdr commented Oct 21, 2026

Copy link
Copy Markdown
Member

Alternative to #83778 and earlier changes in #83509 and #81952.

Removes completely the useIframeDismissalBridge logic added in these PRs. The ui package now doesn't have any awareness or handling of cross-iframe issues.

Instead, the editor canvas iframe in block-editor now forwards the pointerdown, mousedown and click events from the inner iframe document to the outer iframe element. There is an existing bubble logic there to support drag&drop and keyboard shortcuts, we are merely adding a few new events to it.

I tested this on the tag combobox in the post inspector. Opening an autocomplete popup and then clicking in the canvas closes it, as expected. That didn't work before this PR.

Screen.Recording.2026-10-21.at.9.10.48.mov

Conceptually this is a much simpler solution, let's verify that it really works for all cases. For example, in #83509 PR description, @t-hamano has a screencast with a "Canvas popover" and "Sidebar popover". This fix works only when the "Sidebar popover" is open and we click into the canvas. The other way around, a "Canvas popover" open and clicking outside the canvas, it's not fixed. But do we have an actual usage of this in Gutenberg? I think that all popovers should be portaled from the canvas into the top-level window.

@jsnajdr
jsnajdr requested review from ciampo, mirka and t-hamano October 21, 2026 07:15
@jsnajdr jsnajdr self-assigned this Oct 21, 2026
@jsnajdr
jsnajdr requested review from a team and ellatrix as code owners October 21, 2026 07:15
@jsnajdr jsnajdr added the [Type] Bug An existing feature does not function as intended label Oct 21, 2026
@github-actions github-actions Bot added [Package] Block editor /packages/block-editor [Package] UI /packages/ui labels Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

Preview in WordPress Playground

🎉 Props

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: jsnajdr <jsnajdr@git.wordpress.org>
Co-authored-by: mirka <0mirka00@git.wordpress.org>
Co-authored-by: ciampo <mciampini@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Updated as activity occurs, without notifying anyone named here. Add the props-bot label to refresh.

📦 Bundle size

Size Change: -4.14 kB (-0.05%)

Total Size: 9.05 MB

📦 View Changed
Filename Size Change
build/scripts/block-editor/index.min.js 605 kB -872 B (-0.14%)
build/scripts/components/index.min.js 371 kB -785 B (-0.21%)
build/scripts/edit-site/index.min.js 422 kB -719 B (-0.17%)
build/scripts/editor/index.min.js 618 kB -864 B (-0.14%)
build/scripts/media-utils/index.min.js 264 kB -896 B (-0.34%)

e92200b Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 9dff15a trunk % Change
timeToFirstByte 34.95 ms +16.17% -7.15% 35.95 ms +9.6% -5.29% -2.78%
largestContentfulPaint 62 ms +9.68% -9.68% 64 ms +6.25% -6.25% -3.12%
lcpMinusTtfb 24.55 ms +14.46% -2.65% 26 ms +18.27% -4.81% -5.58%
wpBeforeTemplate 18.4 ms +24.62% -5% 19.4 ms +13.14% -7.01% -5.15%
wpTemplate 12.68 ms +5.91% -4.42% 13.28 ms +4.74% -5.42% -4.52%
wpTotal 31.65 ms +15.07% -6.19% 32.95 ms +8.25% -5.31% -3.95%
wpMemoryUsage 7.66 MB +0% -0% 7.63 MB +0% -0% 0.43%
wpDbQueries 16 +0% -0% 16 +0% -0% 0%

front-end-classic-theme

Metric 9dff15a trunk % Change
timeToFirstByte 25.95 ms +7.13% -1.93% 31.1 ms +2.73% -7.07% -16.56%
largestContentfulPaint 60 ms +0% -0% 68 ms +5.88% -0% -11.76%
lcpMinusTtfb 34.4 ms +1.16% -2.47% 38.75 ms +2.58% -6.06% -11.23%
wpBeforeTemplate 16.02 ms +2.56% -1.62% 16.88 ms +5.04% -3.32% -5.09%
wpTemplate 7.31 ms +11.22% -2.19% 10.11 ms +14.05% -2.18% -27.7%
wpTotal 23.67 ms +7.39% -2.32% 28.37 ms +3.42% -7.37% -16.57%
wpMemoryUsage 6.24 MB +0% -0% 6.32 MB +0% -0% -1.23%
wpDbQueries 9 +0% -0% 13 +0% -0% -30.77%

media-processing

Metric 9dff15a trunk % Change
mediaProcessingJpeg 223.44 ms +1.45% -0.56% 227.51 ms +1.51% -1.1% -1.79%
mediaProcessingAvif 3641.07 ms +0.34% -0.94% 3679.78 ms +0.69% -1.68% -1.05%
mediaProcessingJpegToAvif 2533.85 ms +0.66% -0.35% 2565.18 ms +2.25% -1.29% -1.22%

media-upload

Metric 9dff15a trunk % Change
jpegUploadProcessing 1159.18 ms +19.68% -21.55% 1381.56 ms +1.24% -31.63% -16.1%
pngUploadProcessing 162.68 ms +3.2% -4.93% 174.86 ms +5.15% -5.15% -6.97%
largeJpegUploadProcessing 933.2 ms +48.94% -2.15% 1397.48 ms +0.95% -1.79% -33.22%
multipleImageUploadProcessing 1476.13 ms +0.7% -0.82% 1483.86 ms +0.71% -0.97% -0.52%

post-editor

Metric 9dff15a trunk % Change
serverResponse 543.63 ms +6.32% -7.93% 513.78 ms +4.8% -4.8% 5.81%
firstPaint 485.66 ms +8.48% -39.4% 280.59 ms +8.95% -13.67% 73.09%
domContentLoaded 1242.92 ms +3.27% -3.21% 1250.29 ms +0.51% -5.1% -0.59%
loaded 1244.13 ms +3.28% -3.19% 1251.46 ms +0.51% -5.08% -0.59%
firstContentfulPaint 516.7 ms +7.68% -6.93% 533.06 ms +4.61% -3.66% -3.07%
firstBlock 3768.68 ms +2.42% -3.68% 3819.02 ms +0.9% -3.84% -1.32%
type 18.44 ms +5.1% -8.79% 18.94 ms +4.33% -4.07% -2.64%
typeWithoutInspector 18.6 ms +1.08% -2.53% 18.22 ms +9.22% -1.92% 2.09%
typeWithTopToolbar 25.36 ms +10.84% -5.8% 23.74 ms +6.82% -6.44% 6.82%
typeContainer 8.5 ms +15.41% -8% 8.57 ms +9.68% -4.32% -0.82%
focus 135.22 ms +1.93% -1.77% 131.91 ms +7.38% -5.53% 2.51%
firstFocus 249.8 ms +0% -0% 244.73 ms +0% -0% 2.07%
selectAll 604.96 ms +0.03% -2.12% 596.54 ms +1.22% -2.94% 1.41%
listViewOpen 86.07 ms +7.62% -3.71% 86.13 ms +10.5% -3.51% -0.07%
inserterOpen 41.97 ms +3.03% -11.06% 40.68 ms +21.34% -4.01% 3.17%
inserterHover 13.08 ms +9.4% -5.05% 12.17 ms +9.94% -7.23% 7.48%
inserterSearch 8.29 ms +10.01% -10.01% 8.34 ms +13.31% -8.27% -0.6%
loadPatterns 669.41 ms +5.06% -6.95% 680.48 ms +1.78% -2.76% -1.63%
wpTotal 532.91 ms +6.01% -7.98% 503.41 ms +4.88% -4.97% 5.86%
wpMemoryUsage 14.70 MB +0% -0% 14.66 MB +0% -0% 0.24%
wpDbQueries 54.5 +0.92% -0.92% 55 +0% -1.82% -0.91%

site-editor

Metric 9dff15a trunk % Change
serverResponse 514.64 ms +4.94% -3.11% 526.13 ms +3.83% -7.68% -2.18%
firstPaint 248.05 ms +11.24% -6.65% 256.27 ms +7.11% -11.62% -3.21%
domContentLoaded 1308.18 ms +0.38% -0.19% 1278.23 ms +1.39% -1.64% 2.34%
loaded 1309.31 ms +0.39% -0.18% 1279.58 ms +1.37% -1.65% 2.32%
firstContentfulPaint 483.16 ms +9.64% -16.16% 511.28 ms +0.91% -4.01% -5.5%
firstBlock 4878.52 ms +0.22% -0.56% 4778.96 ms +1.14% -0.5% 2.08%
type 20.59 ms +8.94% -5.83% 18.75 ms +4.75% -1.76% 9.81%
navigate 132.41 ms +6.83% -2.51% 116.87 ms +9.41% -3.43% 13.3%
loadPatterns 1337.82 ms +10.61% -4.69% 1268.67 ms +5.03% -2.32% 5.45%
loadPages 1026.04 ms +2.5% -3.63% 1003.93 ms +3.97% -2.47% 2.2%
wpTotal 505.04 ms +4.99% -3.12% 516.56 ms +3.87% -7.9% -2.23%
wpMemoryUsage 12.71 MB +0% -0% 12.68 MB +0% -0% 0.27%
wpDbQueries 44 +2.27% -0% 44 +2.27% -0% 0%

e92200b Run

🏁 Flaky tests

Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

Regression: updating a specific option will only change its value and will not corrupt others in /test/e2e/specs/editor/various/core-settings.spec.js, passed after 1 failed attempt.
Error: expect(received).toStrictEqual(expected) // deep equality

- Expected  - 1
+ Received  + 1

  Array [
    "rewrite_rules",
-   "SERIALIZED DATA",
+   "",
  ]
    at forEach (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/core-settings.spec.js:46:26)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/core-settings.spec.js:43:35
drags a multi-selection from a block nested in a selected block in /test/e2e/specs/editor/various/draggable-blocks.spec.js, passed after 1 failed attempt.
TypeError: Cannot read properties of null (reading 'y')
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:989:57
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:988:3
drags a multi-selection from anywhere inside it in /test/e2e/specs/editor/various/draggable-blocks.spec.js, passed after 1 failed attempt.
TypeError: Cannot read properties of null (reading 'y')
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:824:57
    at dragSelectionBelowLanding (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:823:3)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:844:3
keeps the blocks selected after dropping a multi-selection in /test/e2e/specs/editor/various/draggable-blocks.spec.js, passed after 1 failed attempt.
TypeError: Cannot read properties of null (reading 'y')
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:824:57
    at dragSelectionBelowLanding (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:823:3)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:908:3
refuses the drop and uploads nothing in /test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js, passed after 1 failed attempt.
Error: expect(received).toHaveLength(expected)

Expected length: 0
Received length: 1
Received array:  [{"_links": {"about": [{"href": "http://localhost:8889/wp-json/wp/v2/types/attachment"}], "author": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/users/1"}], "collection": [{"href": "http://localhost:8889/wp-json/wp/v2/media"}], "curies": [{"href": "https://api.w.org/{rel}", "name": "wp", "templated": true}], "replies": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/comments?post=223"}], "self": [{"href": "http://localhost:8889/wp-json/wp/v2/media/223", "targetHints": {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE"]}}], "wp:attached-to": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/posts/222", "id": 222, "post_type": "post"}]}, "alt_text": "", "author": 1, "caption": {"rendered": ""}, "class_list": ["post-223", "attachment", "type-attachment", "status-inherit", "hentry", "entry"], "comment_status": "open", "date": "2026-10-21T08:38:41", "date_gmt": "2026-10-21T08:38:41", "description": {"rendered": "<p class=\"attachment\"><a href='http://localhost:8889/wp-content/uploads/2026/10/d93e819f-8efb-4dc6-8e6a-6613bb9fc957.png'><img loading=\"lazy\" decoding=\"async\" width=\"10\" height=\"10\" src=\"http://localhost:8889/wp-content/uploads/2026/10/d93e819f-8efb-4dc6-8e6a-6613bb9fc957.png\" class=\"attachment-medium size-medium\" alt=\"\" style=\"width:100%;height:100%;max-width:10px;\" /></a></p>
"}, "featured_media": 0, "filename": "d93e819f-8efb-4dc6-8e6a-6613bb9fc957.png", "filesize": 80, "guid": {"rendered": "http://localhost:8889/wp-content/uploads/2026/10/d93e819f-8efb-4dc6-8e6a-6613bb9fc957.png"}, "id": 223, "link": "http://localhost:8889/?attachment_id=223", "media_details": {"file": "2026/10/d93e819f-8efb-4dc6-8e6a-6613bb9fc957.png", "filesize": 80, "height": 10, "image_meta": {"alt": "", "aperture": "0", "camera": "", "caption": "", "copyright": "", "created_timestamp": "0", "credit": "", "focal_length": "0", "iso": "0", "keywords": [], "orientation": "0", "shutter_speed": "0", "title": ""}, "sizes": {}, "width": 10}, "media_type": "image", "meta": [], "mime_type": "image/png", "modified": "2026-10-21T08:38:41", "modified_gmt": "2026-10-21T08:38:41", "ping_status": "closed", "post": 222, "slug": "d93e819f-8efb-4dc6-8e6a-6613bb9fc957", "source_url": "http://localhost:8889/wp-content/uploads/2026/10/d93e819f-8efb-4dc6-8e6a-6613bb9fc957.png", "status": "inherit", "template": "", "title": {"rendered": "d93e819f-8efb-4dc6-8e6a-6613bb9fc957"}, "type": "attachment"}]
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js:71:45
restores the blocks when a multi-selection drag is canceled in /test/e2e/specs/editor/various/draggable-blocks.spec.js, passed after 1 failed attempt.
TypeError: Cannot read properties of null (reading 'y')
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:824:57
    at dragSelectionBelowLanding (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:823:3)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:871:3
should cut and paste individual blocks with collapsed selection in /test/e2e/specs/editor/various/copy-cut-paste.spec.js, passed after 1 failed attempt.
Error: expect(string).toMatchSnapshot(expected) failed

  @@ -1,3 +1,3 @@
   <!-- wp:paragraph -->
  -<p>2Cut - collapsed selection</p>
  +<p>2</p>
   <!-- /wp:paragraph -->


    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/copy-cut-paste.spec.js:45:49

From 38fbef8, not the current head. Run

@Mamaduka

Copy link
Copy Markdown
Member

Oh, love this simple solution!

@mirka mirka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for looking into this, I agree this approach is much simpler for our needs!

Comment thread packages/block-editor/src/components/iframe/index.tsx Outdated
Comment on lines +63 to +70
* Bubbles selected native events from the iframe document onto the outer
* `<iframe>` element so parent-document listeners can observe them.
*
* `dragover` and `mousemove` keep drag-and-drop and pointer tracking working
* across the frame boundary. `pointerdown`, `mousedown`, and `click` let
* parent-document popups (menus, selects, etc.) treat a canvas press as an
* outside press. Keyboard shortcuts are bubbled separately from the React
* `onKeyDown` handler on the iframe element.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With this, a press in the canvas reaches Base UI as a press on the <iframe> element, not on what was actually pressed. That's fine for popups whose trigger is outside the canvas, but not for a control rendered inside it.

For example, put a Combobox in a block's edit UI. Its popup renders in the parent document through the overlay slot, while its input stays in the canvas. Type to open the list, then click inside the input to move the caret: the list closes, because Base UI sees a press on the <iframe>, which is outside both the input and the popup. I reproduced this with a browser test that bubbles presses the same way. Clicking elsewhere in the canvas closes it correctly.

Nothing in the repo does this yet, so I don't think it blocks this PR. We kind of discussed this the other day, and that we may be ok with just having a large chunk of wp-components/wp-ui be off limits in the editor iframe. We can figure out safeguards around this as a follow up.

For the scope of this PR, can we perhaps note the limitation here, so it isn't rediscovered as a bug?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Type to open the list, then click inside the input to move the caret: the list closes, because Base UI sees a press on the <iframe>, which is outside both the input and the popup.

How does the current iframe bridge solution solve this? There is a isInsideCurrentPopup check that bubbles only events that are outside the popup. There is an onOpenChange handler that stores the "trigger" element for the purposed of this checking.

Maybe we could implement a similar check, and filter the bubbled events.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How does the current iframe bridge solution solve this?

It doesn't, and what we discovered in the other PR was that it can't (at least in a sensible way).

Comment thread packages/block-editor/src/components/iframe/index.tsx Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The ui bridge also covered the site preview iframes (SitePreview in edit-site and boot), which don't bubble events. Should they get the same bubbling, or are we fine leaving that gap since they only render for classic themes on the home route?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

SitePreview is interesting. It's a plain <iframe src={homeUrl} />, no portals, no srcdoc. homeUrl is a same-origin URL, but not always. On WordPress.com Simple the admin can be example.wordpress.com/wp-admin while the site is example.blog, a mapped domain.

It's more like a cross-origin embed, which we can't handle anyway. Make we can sacrifice these frames, I don't know how big the practical issue is.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We're removing the iframe tests in ui but add no coverage for the bubbling that replaces them. Should we add a minimal browser or e2e test where a non-modal popup in the parent closes on a canvas press, and the press still reaches its canvas target once?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good idea, I added an e2e test for closing the tags popup. Fits very nicely into the existing taxonomies.spec.js suites that tests that UI.

Which other parts of the Gutenberg UI can help us test the other variants? So that we cover the entire Autocomplete/Combobox/Popover/Menu spectrum?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The tags input uses SearchableChipSelectControl, so the new test covers Combobox. For Menu, we could open the editor's Options menu and click the canvas. That already dismisses through useIframeDismissalBridge, so the test would protect the behavior we're preserving when we remove that workaround.

For Popover, the block inspector's style-state info popover (e.g. "More information about Hover" for a Button block) is a possible example, but it also dismisses on hover leave. We'd need to ensure the test actually depends on the canvas press. In human testing, a keyboard interaction (instead of mouse click) to open the infotip would work I think, but not sure if that will translate correctly to an automated test.

I haven't found a suitable @wordpress/ui Autocomplete popup in the editor. The workflow palette uses it inline inside a modal.

I think Tags plus Options would give us useful e2e coverage. Could we also add a focused browser test around the block-editor Iframe to assert that the canvas target receives the click exactly once? The tags test currently only checks dismissal.

@jsnajdr
jsnajdr force-pushed the update/iframe-popup-closing branch from 1745d6d to 5b8235d Compare October 22, 2026 10:43
timers: vi,
};

// jsdom does not implement DragEvent. A stub is enough for code that only

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think my preference would be for tests that require draft events to just run in Vitest browser mode, any reason why you'd want to stick to JSDom instead?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's existing JSDOM tests starting to fail, without any change. The reason is that the iframe/index.tsx now references DragEvent statically:

const BRIDGED_EVENTS = [
  { native: 'dragover', react: 'onDragOver', ctor: DragEvent },
]

Merely loading the file as a part of the import tree now fails, we don't need to use it.

Maybe we could extract the constructor from the prototype of the original event. And avoid the ctor field.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Pushed a version that removes the need for ctor: DragEvent code and for the JSDOM polyfill. The constructor name is now extracted from the event itself.

@jsnajdr
jsnajdr force-pushed the update/iframe-popup-closing branch from 8100acc to e92200b Compare October 22, 2026 14:13

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Block editor /packages/block-editor [Package] UI /packages/ui [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants