Components: Add overlay legacy slot infrastructure#77755
Components: Add overlay legacy slot infrastructure#77755
Conversation
Add a `getOverlayLegacySlot()` helper and matching `.wp-overlay-legacy` styles. The helper lazily mounts a body-level container in the top-level document with `z-index: 99997` and `isolation: isolate`, establishing a stacking context for legacy `@wordpress/components` overlays (Modal, Popover, Tooltip, Snackbar, Draggable clone) in a follow-up. The slot sits below the WP admin bar (99,999) and below the overlay prime slot used by `@wordpress/ui` leaf overlays (99,998). Per-overlay z-indexes inside the slot continue to control relative ordering, but those values stack relative to the slot rather than the document body. No consumers are wired up yet; nothing portals to the slot.
Group the helper, styles, and test under a single `overlay-legacy-slot/` folder in `utils/`, mirroring the existing component-folder convention (`index`, `style.scss`, `test/`).
|
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 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. |
|
Size Change: +127 B (0%) Total Size: 7.82 MB 📦 View Changed
ℹ️ View Unchanged
|
|
Flaky tests detected in 2f2483e. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/25064244529
|
|
@mirka should we also enable this slot via a flag? Same flag that will enable the prime slot, or different one? If we don't document it (or we explicitly do as private), will we ever be able to remove it at a later point for the |
| // `position: fixed; top: 0; left: 0` (with no width/height) makes the slot | ||
| // a zero-sized anchor at the viewport origin. Descendants positioned with | ||
| // viewport-relative coordinates (via floating-ui or similar) resolve |
There was a problem hiding this comment.
Do you have specific examples of why these position styles are needed? Curious because normal overlay portals (Base UI, Ariakit) don't have these styles.
| // correctly. `isolation: isolate` creates a stacking context regardless of | ||
| // the slot's positioning. The slot has no size, so it never intercepts | ||
| // pointer events on its own. | ||
| .wp-overlay-legacy { |
There was a problem hiding this comment.
I think we should probably use CSS modules for the styling?
| top: 0; | ||
| left: 0; | ||
| z-index: 99997; | ||
| isolation: isolate; |
There was a problem hiding this comment.
One problem I just noticed about isolating the legacy slot is that there are non-wp-components elements that participate in the stacking order with high z-index values, for example:
gutenberg/packages/base-styles/_z-index.scss
Lines 63 to 71 in ae954bf
If any of these non-wp-components elements (including third-party) happened to be coordinating high z-indexes with certain wp-components, we can't just move the wp-components into the legacy portal and leave other elements (especially third-party) behind. The coordination would break.
In other words, perhaps all unsafe legacy elements need to stay in the same global stacking context.
If that's true, we might need to have just one prime slot (no legacy slot), which is positioned above the highest known z-index value (currently 1000000000 for components-draggable__clone). This would make our prime slot higher than both the admin bar and media modal. (Did we identify any blockers to stacking the prime slot above the admin bar? I can't remember, and it's not written anywhere in our notes. I can't think of any blockers at the moment.)
What do you think?
| /** | ||
| * Class name applied to the overlay legacy slot element. | ||
| */ | ||
| export const OVERLAY_LEGACY_SLOT_CLASSNAME = 'wp-overlay-legacy'; |
There was a problem hiding this comment.
I think the identifier should be a data attribute instead of a CSS class.
What?
Adds a
getOverlayLegacySlot()helper plus matching.wp-overlay-legacystyles to@wordpress/components. The helper lazily mounts a body-level container in the top-level document withz-index: 99997andisolation: isolate. No consumers are wired up yet; the slot exists but nothing portals to it.Why?
This is the foundation for a two-slot overlay architecture that will let
@wordpress/componentsand@wordpress/uioverlays compose correctly when mixed:z-index: 99997) — for@wordpress/componentsoverlays (Modal, Popover, Tooltip, Snackbar, Draggable clone). Migrated in follow-ups.z-index: 99998) — for@wordpress/uileaf overlays. Built in follow-ups.Both slots sit below the WP admin bar (99,999). Within each slot, per-overlay z-indexes continue to control relative ordering, but those values now stack relative to the slot rather than the document body.
This PR ships the legacy slot dormant — it can be merged independently and reverted without affecting any user-visible behavior.
How?
packages/components/src/utils/overlay-legacy-slot.ts:getOverlayLegacySlot()returns the slot element, lazily creating<div class=\"wp-overlay-legacy\">on first call and caching the singleton.window.top?.document ?? window.documentso overlays portaling from inside an iframe land in the parent document's slot. Falls back to the current document when cross-origin restrictions blockwindow.top.document..wp-overlay-legacyelement already exists in the DOM (e.g., from a previous bundle), it is reused rather than duplicated.packages/components/src/utils/overlay-legacy-slot.scss:.wp-overlay-legacy { position: fixed; top: 0; left: 0; z-index: 99997; isolation: isolate; }position: fixed; top: 0; left: 0(no width/height) makes the slot a zero-sized anchor at the viewport origin so it never intercepts pointer events.isolation: isolatecreates a stacking context regardless of the fixed positioning.Stacked follow-up PRs
Each migrates one legacy overlay onto the slot introduced here. Each builds on the previous branch, so its diff only shows its own changes:
Popover: render the fallback container and Gutenberg's internalPopover.Slotdeclarations inside the legacy slot.Modal: portal modals into the slot; rewrite the aria-hide algorithm to handle the wrapper.Tooltip,Menu,CustomSelectControlv2: passportalElement={ getOverlayLegacySlot }to each Ariakit-backed overlay.SnackbarList: portal Gutenberg shells'<SnackbarNotices>into the slot; bake the previously-mixin-only positioning into.components-snackbar-listand remove thesnackbar-container()SCSS mixin.Draggable: append the drag clone to the slot instead of the document body.Testing Instructions
This PR is dormant infrastructure — there is no user-visible behavior change to verify manually. Verification is via unit tests:
The tests cover: lazy creation on first call, singleton caching across calls, reuse of a pre-existing
.wp-overlay-legacyelement in the DOM, and recreation when the cached element has been detached.Once the follow-up PRs land, editor-level verification becomes meaningful.
Testing Instructions for Keyboard
Not applicable — no UI changes.
Use of AI Tools
This PR was authored with assistance from AI tooling (Claude). All code, copy, and structural decisions were reviewed by a human contributor.