fix: prevent dropdown menus from overflowing the editor root - #562
Draft
dcalhoun wants to merge 3 commits into
Draft
fix: prevent dropdown menus from overflowing the editor root#562dcalhoun wants to merge 3 commits into
dcalhoun wants to merge 3 commits into
Conversation
XCFramework BuildThis PR's XCFramework is available for testing. Add the following to your .package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/562")Built from 2684d95 |
dcalhoun
force-pushed
the
fix/prevent-dropdown-menu-overflow
branch
from
July 23, 2026 18:50
107607c to
b28c1e4
Compare
Block toolbar popovers (e.g. the "Options" menu) render into a fallback container appended to the body and are positioned relative to the viewport. When anchored near the right edge—such as after scrolling the block toolbar—they extend past the viewport, growing the scrollable width of the root element. The canvas could then be swiped horizontally, revealing the empty background behind the editor styles wrapper. Apply `contain: paint` to the body, which fills the viewport, so off-screen popovers are clipped there instead of expanding the document's scrollable width. `overflow-x` on the body does not clip them (the value propagates to the viewport rather than clipping out-of-flow descendants), and clipping on `#root` misses them since the fallback container is a sibling of `#root`, not a descendant. Because `contain: paint` on the body would otherwise clip content taller than the viewport instead of letting it scroll, give `.gutenberg-kit-root` its own vertical scroll so overflowing editor content scrolls there rather than relying on the (now paint-contained) document. Clip at the body rather than moving popovers into a `Popover.Slot` within the editor: popovers must stay in the default fallback container—a sibling of `#root`—so the modalize helper can mark the rest of the editor inert while keeping the active popover accessible when the block inspector or inserter is open (see #145). Document this constraint inline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dcalhoun
force-pushed
the
fix/prevent-dropdown-menu-overflow
branch
from
July 23, 2026 19:20
b28c1e4 to
686e739
Compare
`modalize` hid only the body's children, which assumed the modal element was itself a body child. That holds for popovers in Gutenberg's fallback container, but not for popovers rendered into a slot nested within a body-level container. Walk from each modal element up to the body instead, hiding the other children of every ancestor along the way. Ancestors of a modal element are collected first so they are never hidden, which would hide the modal element with them. Also accept multiple modal elements, as popovers may render into more than one container. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Block toolbar popovers—e.g. the "Options" menu—rendered into Gutenberg's fallback container, a body child positioned relative to the viewport. When anchored near the right edge, such as after scrolling the block toolbar, they extended past the viewport and grew the document's scrollable width. The canvas could then be swiped horizontally, revealing the empty background beside the editor styles wrapper. Render popovers into slots within body-level containers instead, split by whether the popover needs clipping: - Dropdown popovers use the default slot, in a fixed, `overflow: hidden` container. Being out of flow, it clips the overflow without contributing to the document's scrollable size, so the document remains the scroll container. - The full-screen block settings menu opts into its own slot in an unclipped container via `__unstableSlotName`. WebKit renders a `position: fixed` element semi-transparent when an ancestor clips its overflow, and the menu fills the viewport and never overflows, so it does not need clipping. Both containers are body children, so `modalize` keeps the active popover accessible while marking the rest of the document inert. This replaces `contain: paint` on the body, which clipped the overflow but required moving vertical scroll onto `.gutenberg-kit-root`. That broke iOS scroll-to-top, which only drives the web view's main scroll view—the document scroller. Wrap the layout in a `SlotFillProvider` so the slots and the editor's popovers share one registry. `BlockEditorProvider` otherwise creates its own, leaving the slots unreachable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
Opening a block toolbar dropdown menu (e.g. the "Options" more menu) can add horizontal overflow to the editor's root element, letting the canvas be swiped sideways to reveal the empty background behind it.
Why?
Ref CMM-997.
The editor is not iframed on mobile, so the whole editor lives in a single document. Block toolbar popovers render into a fallback container appended to
<body>and are positioned relative to the viewport. When a popover is anchored near the right edge—such as after scrolling the block toolbar so the toggle sits at the far right—it extends past the viewport. Because nothing clips horizontal overflow on<html>/<body>/#root, this grows the scrollable width of the root element, and the canvas can be swiped horizontally to reveal the empty background behind the editor styles wrapper.Measured with the menu open on a 375px-wide viewport: the popover's right edge lands at 419px (44px past the viewport),
html.scrollWidthgrows to 419 whileclientWidthstays 375, and only<html>becomes horizontally scrollable.How?
Render popovers into slots within body-level containers, split by whether the popover needs clipping:
position: fixed; inset: 0; overflow: hiddencontainer. Being out of flow, it clips the overflow without contributing to the document's scrollable size, so the document remains the scroll container. The container spans the viewport, so it setspointer-events: noneand the popovers within restorepointer-events: auto.__unstableSlotName. It fills the viewport and never overflows, so it does not need clipping—and WebKit renders aposition: fixedelement semi-transparent when an ancestor clips its overflow, so keeping it clear of any clipping ancestor avoids that bug outright rather than working around it.Both containers are body children, so
modalizecan keep the active popover accessible while marking the rest of the document inert.A few supporting changes:
modalizenow walks the ancestor path (first commit). It previously hid onlydocument.body's children, which assumed the modal element was itself a body child—true for the fallback container, not for a popover nested in a slot. It now walks from each modal element up to the body, hiding the other children of every ancestor, collecting ancestors first so they are never hidden. It also accepts multiple modal elements.SlotFillProviderso the slots and the editor's popovers share one registry.BlockEditorProviderrenders<SlotFillProvider passthrough>, which creates its own registry when no provider exists above it, leaving the slots unreachable and silently sending popovers back to the fallback container.Alternatives considered
contain: painton the body (the previous approach in this PR) clips the overflow, but it also stops the document from scrolling content taller than the viewport. Restoring vertical scroll on.gutenberg-kit-rootmoves the scroll off the document, which breaks iOS status-bar-tap scroll-to-top—that gesture only drives the web view's main scroll view. Both rules are reverted here, so the document is the scroll container again.overflow-x: hidden/cliponhtml/bodydoes not clip out-of-flow popovers at all (the value propagates to the viewport), and clipping on#rootmisses them entirely since the fallback container is a sibling of#root, not a descendant.Testing Instructions
trunk, the canvas shifts left and reveals the root element's background.)Please also confirm the following, as they cover the constraints this approach had to satisfy:
Accessibility Testing Instructions
Popovers now render into slots rather than Gutenberg's fallback container, and
modalizewas refactored to match, so the inert behaviour from #145 is worth re-checking with VoiceOver:Verified in the DOM: with a popover open,
#rootisaria-hidden="true"while both popover containers, the popover's ancestor chain, and thearia-liveregions are untouched; dismissing removes every attribute that was added.Screenshots or screencast