-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Components: Add overlay legacy slot infrastructure #77755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /** | ||
| * Class name applied to the overlay legacy slot element. | ||
| */ | ||
| export const OVERLAY_LEGACY_SLOT_CLASSNAME = 'wp-overlay-legacy'; | ||
|
|
||
| /** | ||
| * Resolves the top-level document so that overlays portaling from inside an | ||
| * iframe still land in the parent document's slot. Falls back to the current | ||
| * document if `window.top` is inaccessible (cross-origin restrictions). | ||
| */ | ||
| function getTopLevelDocument(): Document { | ||
| try { | ||
| return window.top?.document ?? window.document; | ||
| } catch { | ||
| // Cross-origin access to `window.top.document` throws — use the | ||
| // current frame's document as a safe fallback. | ||
| return window.document; | ||
| } | ||
| } | ||
|
|
||
| let cachedSlot: HTMLDivElement | null = null; | ||
|
|
||
| /** | ||
| * Lazily creates and returns the overlay legacy slot — a body-level container | ||
| * used as the portal target for legacy `@wordpress/components` overlays | ||
| * (Modal, Popover, Tooltip, Snackbar, Draggable clone). | ||
| * | ||
| * The slot has `z-index: 99997` and `isolation: isolate`, establishing a | ||
| * stacking context that 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 | ||
| * (Tooltip > Popover > Modal), but those values now stack relative to the | ||
| * slot rather than the document body. | ||
| * | ||
| * The slot is created in the top-level document (not the iframe's own | ||
| * document) so that overlays portaling from inside an iframe land in the | ||
| * parent document's slot. The element is cached and reused across calls; if | ||
| * the cached element has been detached or belongs to a different document, | ||
| * a fresh element is created. | ||
| * | ||
| * @return The overlay legacy slot element. | ||
| */ | ||
| export function getOverlayLegacySlot(): HTMLDivElement { | ||
| const doc = getTopLevelDocument(); | ||
|
|
||
| if ( | ||
| cachedSlot && | ||
| cachedSlot.isConnected && | ||
| cachedSlot.ownerDocument === doc | ||
| ) { | ||
| return cachedSlot; | ||
| } | ||
|
|
||
| const existing = doc.body.querySelector< HTMLDivElement >( | ||
| `.${ OVERLAY_LEGACY_SLOT_CLASSNAME }` | ||
| ); | ||
| if ( existing ) { | ||
| cachedSlot = existing; | ||
| return existing; | ||
| } | ||
|
|
||
| const slot = doc.createElement( 'div' ); | ||
| slot.className = OVERLAY_LEGACY_SLOT_CLASSNAME; | ||
| doc.body.append( slot ); | ||
| cachedSlot = slot; | ||
| return slot; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||||||||||||||||||
| // Overlay legacy slot — body-level container that establishes a stacking | ||||||||||||||||||||
| // context for legacy `@wordpress/components` overlays (Modal, Popover, | ||||||||||||||||||||
| // Tooltip, Snackbar, Draggable clone). | ||||||||||||||||||||
| // | ||||||||||||||||||||
| // Sits below the WP admin bar (99,999) and below the overlay prime slot | ||||||||||||||||||||
| // used by `@wordpress/ui` leaf overlays (99,998). Declared outside any CSS | ||||||||||||||||||||
| // layer for predictable plugin-override semantics. | ||||||||||||||||||||
| // | ||||||||||||||||||||
| // `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 | ||||||||||||||||||||
|
Comment on lines
+9
to
+11
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 { | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should probably use CSS modules for the styling? |
||||||||||||||||||||
| position: fixed; | ||||||||||||||||||||
| top: 0; | ||||||||||||||||||||
| left: 0; | ||||||||||||||||||||
| z-index: 99997; | ||||||||||||||||||||
| isolation: isolate; | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 What do you think?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that you have a point. I'll get back to the drawing board |
||||||||||||||||||||
| } | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { getOverlayLegacySlot, OVERLAY_LEGACY_SLOT_CLASSNAME } from '..'; | ||
|
|
||
| describe( 'getOverlayLegacySlot', () => { | ||
| afterEach( () => { | ||
| document.body | ||
| .querySelectorAll( `.${ OVERLAY_LEGACY_SLOT_CLASSNAME }` ) | ||
| .forEach( ( element ) => element.remove() ); | ||
| } ); | ||
|
|
||
| it( 'lazily creates a slot element on first call and appends it to the document body', () => { | ||
| expect( | ||
| document.body.querySelector( `.${ OVERLAY_LEGACY_SLOT_CLASSNAME }` ) | ||
| ).toBeNull(); | ||
|
|
||
| const slot = getOverlayLegacySlot(); | ||
|
|
||
| expect( slot ).toBeInstanceOf( HTMLDivElement ); | ||
| expect( slot ).toHaveClass( OVERLAY_LEGACY_SLOT_CLASSNAME ); | ||
| expect( slot.parentElement ).toBe( document.body ); | ||
| } ); | ||
|
|
||
| it( 'caches the singleton across calls', () => { | ||
| const first = getOverlayLegacySlot(); | ||
| const second = getOverlayLegacySlot(); | ||
|
|
||
| expect( second ).toBe( first ); | ||
| expect( | ||
| document.body.querySelectorAll( | ||
| `.${ OVERLAY_LEGACY_SLOT_CLASSNAME }` | ||
| ) | ||
| ).toHaveLength( 1 ); | ||
| } ); | ||
|
|
||
| it( 'reuses an existing slot element already present in the DOM', () => { | ||
| const preexisting = document.createElement( 'div' ); | ||
| preexisting.className = OVERLAY_LEGACY_SLOT_CLASSNAME; | ||
| document.body.append( preexisting ); | ||
|
|
||
| const slot = getOverlayLegacySlot(); | ||
|
|
||
| expect( slot ).toBe( preexisting ); | ||
| } ); | ||
|
|
||
| it( 'recreates the slot if the cached element has been detached', () => { | ||
| const first = getOverlayLegacySlot(); | ||
| first.remove(); | ||
|
|
||
| const second = getOverlayLegacySlot(); | ||
|
|
||
| expect( second ).not.toBe( first ); | ||
| expect( second.isConnected ).toBe( true ); | ||
| expect( second.parentElement ).toBe( document.body ); | ||
| } ); | ||
| } ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the identifier should be a data attribute instead of a CSS class.