From 7768e436daea5154f09470fb995883d289e98861 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 12 May 2026 11:06:38 +0200 Subject: [PATCH 01/27] Draggable: Migrate clone wrapper to wp compat overlay slot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the legacy body-level / element-wrapper placement and its `z-index: 1000000000` with a portal-style migration onto the `@wordpress/ui` compat overlay slot (#77851). When the slot is available, the drag clone joins the slot's body-level stacking context across all three placement modes, so an active drag automatically shares stacking with any `@wordpress/ui` overlay opened mid-drag without needing per-version z-index races. Auto-enabled in WordPress environments via the slot helper's `window.wp.components` auto-detect; standalone hosts that bundle `@wordpress/components` directly fall back to the previous placement until they call `useEnableWpCompatOverlaySlot()`. `@wordpress/components` imports `getWpCompatOverlaySlot()` directly from `@wordpress/ui`'s public exports (also promoted from internal in this change). The `@wordpress/components` dep on `@wordpress/ui` is transitional, scoped to the legacy-overlay migration. Cross-document drags (e.g. dragging an element inside an iframe while the slot is in the parent document) fall back to the previous placement so the clone's viewport-relative geometry stays in a single coordinate space. The default placement mode (`appendToOwnerDocument: false`, no `dragComponent`) previously appended the clone to the dragged element's parent. In WP environments where the slot is now in effect, the clone instead lives in the slot — a body-level location. In-repo ripgrep finds no CSS or event-delegation scoping anchored to the clone's previous in-flow parent; external consumers that relied on that ancestry must either not opt into the slot or migrate their scoping. --- package-lock.json | 1 + packages/components/CHANGELOG.md | 3 +- packages/components/package.json | 1 + packages/components/src/draggable/index.tsx | 16 +- packages/components/src/draggable/types.ts | 7 + packages/components/tsconfig.json | 1 + packages/ui/CHANGELOG.md | 2 +- packages/ui/src/index.ts | 1 + .../ui/src/popover/stories/index.story.tsx | 97 ------------- packages/ui/tsconfig.json | 3 +- ...raggable-cross-document-fallback.story.jsx | 137 ++++++++++++++++++ .../popover-with-slotfill.story.jsx | 109 ++++++++++++++ 12 files changed, 272 insertions(+), 106 deletions(-) create mode 100644 storybook/stories/playground/draggable-cross-document-fallback.story.jsx create mode 100644 storybook/stories/playground/popover-with-slotfill.story.jsx diff --git a/package-lock.json b/package-lock.json index 5878c4c8cb60d3..154a657fa3c985 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59726,6 +59726,7 @@ "@wordpress/private-apis": "file:../private-apis", "@wordpress/rich-text": "file:../rich-text", "@wordpress/style-runtime": "file:../style-runtime", + "@wordpress/ui": "file:../ui", "@wordpress/warning": "file:../warning", "change-case": "^4.1.2", "clsx": "^2.1.1", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2998bb71ddbdcb..9c05f2ce172e2c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -10,6 +10,7 @@ - `Button`: Align `link` variant underline (offset and thickness) with `ExternalLink` and `Link` from `@wordpress/ui` ([#77842](https://github.com/WordPress/gutenberg/pull/77842)). - `Modal`: render as a bottom sheet on mobile so the height adapts to the content and CTAs stay within thumb reach ([#77956](https://github.com/WordPress/gutenberg/pull/77956)). - `Text`: Use a theme-aware gray token for the muted variant ([#77999](https://github.com/WordPress/gutenberg/pull/77999)). +- `Draggable`: Migrate the drag clone into the `@wordpress/ui` compat overlay slot so an active drag shares stacking with `@wordpress/ui` overlays opened mid-drag. Auto-enabled in WordPress environments (where `window.wp.components` is on the global); other hosts fall back to the previous placement until they call `useEnableWpCompatOverlaySlot()`. Cross-document drags (dragging an element inside an iframe while the slot is in the parent document) also fall back to the previous placement so the clone's viewport-relative geometry stays in a single coordinate space ([#78183](https://github.com/WordPress/gutenberg/pull/78183)). ### Bug Fixes @@ -21,11 +22,9 @@ ### Internal -- `Modal`, `Menu`, `DropdownMenu`: Adopt `--wpds-motion-easing-*` and `--wpds-motion-duration-*` design tokens for animation timing ([#76097](https://github.com/WordPress/gutenberg/pull/76097)). - Update `date-fns` dependency to `v4.1.0` ([#78057](https://github.com/WordPress/gutenberg/pull/78057)). - Update code to comply with `eslint-plugin-react-hooks` v7 ([#69962](https://github.com/WordPress/gutenberg/pull/69962)). - `SlotFill`: Add dependencies to `updateFill` effect ([#77907](https://github.com/WordPress/gutenberg/pull/77907)). -- `Popover`: Remove unnecessary close button z-index style ([#78180](https://github.com/WordPress/gutenberg/pull/78180)). ## 33.0.0 (2026-04-29) diff --git a/packages/components/package.json b/packages/components/package.json index 722fb737f69680..3d13e67664bebf 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -82,6 +82,7 @@ "@wordpress/private-apis": "file:../private-apis", "@wordpress/rich-text": "file:../rich-text", "@wordpress/style-runtime": "file:../style-runtime", + "@wordpress/ui": "file:../ui", "@wordpress/warning": "file:../warning", "change-case": "^4.1.2", "clsx": "^2.1.1", diff --git a/packages/components/src/draggable/index.tsx b/packages/components/src/draggable/index.tsx index b9dea8fed43061..f6f30599e49586 100644 --- a/packages/components/src/draggable/index.tsx +++ b/packages/components/src/draggable/index.tsx @@ -8,6 +8,7 @@ import type { DragEvent } from 'react'; */ import { throttle } from '@wordpress/compose'; import { useEffect, useRef } from '@wordpress/element'; +import { getWpCompatOverlaySlot } from '@wordpress/ui'; /** * Internal dependencies @@ -99,6 +100,13 @@ export function Draggable( { */ function start( event: DragEvent ) { const { ownerDocument } = event.target as HTMLElement; + // When enabled, the compat overlay slot takes precedence over + // the legacy placement modes — but only when it lives in the + // same document as the dragged element, so the clone's + // viewport-relative geometry resolves in a single coordinate + // space. + const slot = getWpCompatOverlaySlot(); + const compatSlot = slot?.ownerDocument === ownerDocument ? slot : null; event.dataTransfer.setData( transferDataType, @@ -141,8 +149,7 @@ export function Draggable( { clonedDragComponent.innerHTML = dragComponentRef.current.innerHTML; cloneWrapper.appendChild( clonedDragComponent ); - // Inject the cloneWrapper into the DOM. - ownerDocument.body.appendChild( cloneWrapper ); + ( compatSlot ?? ownerDocument.body ).appendChild( cloneWrapper ); } else { const element = ownerDocument.getElementById( elementId @@ -173,8 +180,9 @@ export function Draggable( { cloneWrapper.appendChild( clone ); - // Inject the cloneWrapper into the DOM. - if ( appendToOwnerDocument ) { + if ( compatSlot ) { + compatSlot.appendChild( cloneWrapper ); + } else if ( appendToOwnerDocument ) { ownerDocument.body.appendChild( cloneWrapper ); } else { elementWrapper?.appendChild( cloneWrapper ); diff --git a/packages/components/src/draggable/types.ts b/packages/components/src/draggable/types.ts index 61dd345e74d2ca..0ea72531d75897 100644 --- a/packages/components/src/draggable/types.ts +++ b/packages/components/src/draggable/types.ts @@ -21,6 +21,13 @@ export type DraggableProps = { * Whether to append the cloned element to the `ownerDocument` body. * By default, elements sourced by id are appended to the element's wrapper. * + * Note: this prop has no effect when the `@wordpress/ui` compat overlay + * slot is in effect and lives in the same document as the dragged + * element. In that case the clone is placed in the slot regardless of + * this prop. Cross-document drags (e.g. dragging an element inside an + * iframe while the slot is in the parent document) fall back to this + * prop's regular semantics. + * * @default false */ appendToOwnerDocument?: boolean; diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index e86cbfa127ae2e..2b640ad8a293ba 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -28,6 +28,7 @@ { "path": "../react-i18n" }, { "path": "../rich-text" }, { "path": "../style-runtime" }, + { "path": "../ui" }, { "path": "../warning" } ], "files": [ "global.d.ts", "storybook.d.ts" ], diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index d35c3aff000c9c..be27e20ca8757c 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -21,7 +21,6 @@ ### Bug Fixes -- `Text`: Provide CSS defense values for every variant when rendered as either a paragraph or heading element ([#78172](https://github.com/WordPress/gutenberg/pull/78172)). - `Autocomplete`, `Select`: Do not show the interactive cursor for disabled select triggers or disabled popup items ([#78112](https://github.com/WordPress/gutenberg/pull/78112)). - `Select`: Hide the browser focus ring on highlighted popup items ([#77919](https://github.com/WordPress/gutenberg/pull/77919)). - `Drawer`: Restore the slide-out animation when the popup closes ([#77800](https://github.com/WordPress/gutenberg/pull/77800)). @@ -41,6 +40,7 @@ - `CollapsibleCard.Header`: Pass `render={

}` (or any of `

`–`

`) to wrap the trigger in a heading and contribute to the document outline, following the W3C APG accordion pattern (heading wraps button) ([#77962](https://github.com/WordPress/gutenberg/pull/77962)). - `Select`: Add a `Select.Positioner` subcomponent and a `positioner` slot prop on `Select.Popup` to customize the popup placement, mirroring the existing `portal` slot pattern ([#78168](https://github.com/WordPress/gutenberg/pull/78168)). - `Autocomplete`: Add an `Autocomplete.Positioner` subcomponent and a `positioner` slot prop on `Autocomplete.Popup` to customize the popup placement, mirroring the existing `portal` slot pattern ([#78168](https://github.com/WordPress/gutenberg/pull/78168)). +- Export `getWpCompatOverlaySlot()` so consumers can route their own portals into the compat overlay slot. Returns the body-level `[data-wp-compat-overlay-slot]` element when the runtime opts in, lazily creating it on first call, and `undefined` otherwise ([#78183](https://github.com/WordPress/gutenberg/pull/78183)). ### Internal diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 7b7efd78856256..1333f3564d2b65 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -17,5 +17,6 @@ export * from './stack'; export * as Tabs from './tabs'; export * from './text'; export * as Tooltip from './tooltip'; +export { getWpCompatOverlaySlot } from './utils/wp-compat-overlay-slot'; export { useEnableWpCompatOverlaySlot } from './utils/use-enable-wp-compat-overlay-slot'; export * from './visually-hidden'; diff --git a/packages/ui/src/popover/stories/index.story.tsx b/packages/ui/src/popover/stories/index.story.tsx index 1795ba74b7e90c..d10c3d47ba04b9 100644 --- a/packages/ui/src/popover/stories/index.story.tsx +++ b/packages/ui/src/popover/stories/index.story.tsx @@ -1,6 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { useId, useRef, useState } from '@wordpress/element'; -import { SlotFillProvider, Slot } from '@wordpress/components'; import { close, info } from '@wordpress/icons'; import * as Popover from '../'; import { VisuallyHidden } from '../../visually-hidden'; @@ -697,102 +696,6 @@ export const CrossIframe: Story = { }, }; -/** - * Same cross-iframe scenario, but using `SlotFillProvider` and `Slot` from - * `@wordpress/components` as the render target. - * - * The `Slot` renders a `div` in the parent document, and its forwarded ref - * is passed to `Popover.Portal`'s `container` prop (via `Popover.Popup`'s - * `portal` prop) so the popup portals into the slot element. This mirrors the - * legacy Popover's `WithSlotOutsideIframe` pattern. - */ -export const CrossIframeWithSlotFill: Story = { - name: 'Cross-Iframe (SlotFill)', - args: { defaultOpen: true }, - argTypes: { defaultOpen: { control: false } }, - render: function Render( { children: _children, ...args } ) { - const slotRef = useRef< HTMLDivElement >( null ); - const [ iframeBoundary, setIframeBoundary ] = - useState< HTMLIFrameElement | null >( null ); - - return ( - - - -
-
- - - Popover's anchor (inside iframe) - - - } - /> - } - positioner={ - - } - > - - - Cross-Iframe (SlotFill) - - - This popup renders in the parent - document via a `Slot` from - `@wordpress/components`. - - - -
-
-
-
- ); - }, -}; - /** * Popovers in Gutenberg are managed with explicit z-index values, which can * create situations where a popover renders below another popover when you diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 4060c432adb43b..a60938c200756d 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -21,6 +21,5 @@ { "path": "../style-runtime" }, { "path": "../theme" } ], - "files": [ "global.d.ts" ], - "exclude": [] + "files": [ "global.d.ts" ] } diff --git a/storybook/stories/playground/draggable-cross-document-fallback.story.jsx b/storybook/stories/playground/draggable-cross-document-fallback.story.jsx new file mode 100644 index 00000000000000..4e6247f9ddfeb5 --- /dev/null +++ b/storybook/stories/playground/draggable-cross-document-fallback.story.jsx @@ -0,0 +1,137 @@ +/** + * WordPress dependencies + */ +import { Draggable } from '@wordpress/components'; +import { createPortal, useEffect, useRef, useState } from '@wordpress/element'; +import { getWpCompatOverlaySlot } from '@wordpress/ui'; + +/** + * Internal dependencies + */ +import { WithWpCompatOverlaySlot } from './with-wp-compat-overlay-slot'; + +/** + * Cross-document `Draggable` regression coverage: when the + * `@wordpress/ui` compat overlay slot is enabled but lives in a different + * document than the dragged element (slot in the parent document, drag + * happens inside an iframe), `Draggable` falls back to its legacy + * placement instead of routing the clone into the slot. This keeps the + * clone's viewport-relative geometry in a single coordinate space. + * + * Lives in `storybook/stories/playground/` rather than next to the other + * `Draggable` stories because the slot opt-in is a window-level flag + * that's process-wide across the Storybook preview iframe. Co-locating + * this story with `Default` / `AppendElementToOwnerDocument` would leak + * the opt-in into those sibling stories on the shared autodocs page, + * silently changing their placement behavior. Living in its own file + * here keeps the leak blast-radius to this single story page. + */ +export default { + title: 'Playground/Draggable cross-document fallback', + component: Draggable, + decorators: [ WithWpCompatOverlaySlot ], + parameters: { + sourceLink: + 'storybook/stories/playground/draggable-cross-document-fallback.story.jsx', + }, +}; + +export const InsideIframeWithCompatSlot = () => { + const iframeRef = useRef( null ); + const [ iframeBody, setIframeBody ] = useState( null ); + + const updateIframeBody = () => { + setIframeBody( iframeRef.current?.contentDocument?.body ?? null ); + }; + + useEffect( updateIframeBody, [] ); + + return ( +
+

+ Drag the blue handle inside the iframe. The orange clone should + appear under the cursor. If it appears offset toward the + top-left of the Storybook canvas, the compat-slot cross-document + fallback has regressed. +

+

+ + Compat slot present in this document:{ ' ' } + + { String( + typeof window !== 'undefined' && + getWpCompatOverlaySlot() !== null + ) } + + +

+ + ); +} + export const CrossIframeWithSlotFill = { name: 'Cross-Iframe (SlotFill)', args: { defaultOpen: true }, argTypes: { defaultOpen: { control: false } }, render: function Render( { children: _children, ...args } ) { - const slotRef = useRef( null ); + const [ slotNode, setSlotNode ] = useState( null ); const [ iframeBoundary, setIframeBoundary ] = useState( null ); return ( @@ -32,10 +49,10 @@ export const CrossIframeWithSlotFill = { - + } positioner={
- + ); }, From ef629062154ba08578962d532c4f207fe903c945 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 15 May 2026 11:14:57 +0200 Subject: [PATCH 11/27] Storybook: draggable cross-doc story: load components styles via Storybook bundle Swap the iframe's style injection from a `?inline` import of `packages/components/src/draggable/style.scss` (reaching into another package's source) to Storybook's own `storybook/package-styles/components-ltr.lazy.scss`, which is the canonical bundle of `@wordpress/components` styles for stories. The injected CSS is now broader than strictly necessary (the whole package stylesheet rather than only Draggable's rules), but this is a debug fixture and the cost is negligible. In exchange we drop the cross-package src reach. Per mirka's review on PR #78183. --- .../draggable-cross-document-fallback.story.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/storybook/stories/playground/draggable-cross-document-fallback.story.jsx b/storybook/stories/playground/draggable-cross-document-fallback.story.jsx index 8680c229d0c24b..56295e382ce3aa 100644 --- a/storybook/stories/playground/draggable-cross-document-fallback.story.jsx +++ b/storybook/stories/playground/draggable-cross-document-fallback.story.jsx @@ -9,7 +9,7 @@ import { getWpCompatOverlaySlot } from '@wordpress/ui'; * Internal dependencies */ import { WithWpCompatOverlaySlot } from './with-wp-compat-overlay-slot'; -import draggableStyles from '../../../packages/components/src/draggable/style.scss?inline'; +import componentsStyles from '../../package-styles/components-ltr.lazy.scss?inline'; export default { title: 'Playground/Draggable cross-document fallback', @@ -29,11 +29,11 @@ export const InsideIframeWithCompatSlot = () => { const iframeDoc = iframeRef.current?.contentDocument; if ( iframeDoc?.head && - ! iframeDoc.getElementById( 'draggable-iframe-styles' ) + ! iframeDoc.getElementById( 'components-iframe-styles' ) ) { const styleEl = iframeDoc.createElement( 'style' ); - styleEl.id = 'draggable-iframe-styles'; - styleEl.textContent = draggableStyles; + styleEl.id = 'components-iframe-styles'; + styleEl.textContent = componentsStyles; iframeDoc.head.appendChild( styleEl ); } setIframeBody( iframeDoc?.body ?? null ); From 583568885b7cfb38c6ed5084fd9bdb45aeff245e Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 15 May 2026 11:15:32 +0200 Subject: [PATCH 12/27] Storybook: Move cross-document fallback story under "Debug fixtures" The cross-document fallback story is strictly defensive regression coverage and doesn't illustrate a pattern non- maintainers would seek out. Move it under a `Debug fixtures` sub-section in the sidebar so the main `Playground/` namespace stays focused on intended-usage demos. Per mirka's review on PR #78183. --- .../playground/draggable-cross-document-fallback.story.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storybook/stories/playground/draggable-cross-document-fallback.story.jsx b/storybook/stories/playground/draggable-cross-document-fallback.story.jsx index 56295e382ce3aa..dd7825d3ed3750 100644 --- a/storybook/stories/playground/draggable-cross-document-fallback.story.jsx +++ b/storybook/stories/playground/draggable-cross-document-fallback.story.jsx @@ -12,7 +12,7 @@ import { WithWpCompatOverlaySlot } from './with-wp-compat-overlay-slot'; import componentsStyles from '../../package-styles/components-ltr.lazy.scss?inline'; export default { - title: 'Playground/Draggable cross-document fallback', + title: 'Playground/Debug fixtures/Draggable cross-document fallback', component: Draggable, decorators: [ WithWpCompatOverlaySlot ], parameters: { From 54e507024bd56567eaad38917259624b6ac46641 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 15 May 2026 11:31:33 +0200 Subject: [PATCH 13/27] Storybook: Drop redundant `parameters.sourceLink` from playground stories The `source-link` Storybook addon already derives the GitHub source path from `storyData.importPath` when no explicit `parameters.sourceLink` is provided (see `storybook/addons/source-link/manager.ts`). For stories living under `storybook/stories/playground/`, that fallback resolves to the same value the explicit `sourceLink` was hard-coding, so the declaration is pure duplication. Per mirka's review on PR #78183 (empty-suggestion blocks covering the `parameters: { sourceLink: ... }` literal). --- .../playground/draggable-cross-document-fallback.story.jsx | 4 ---- storybook/stories/playground/popover-with-slotfill.story.jsx | 4 ---- 2 files changed, 8 deletions(-) diff --git a/storybook/stories/playground/draggable-cross-document-fallback.story.jsx b/storybook/stories/playground/draggable-cross-document-fallback.story.jsx index dd7825d3ed3750..3beebf80c8e37d 100644 --- a/storybook/stories/playground/draggable-cross-document-fallback.story.jsx +++ b/storybook/stories/playground/draggable-cross-document-fallback.story.jsx @@ -15,10 +15,6 @@ export default { title: 'Playground/Debug fixtures/Draggable cross-document fallback', component: Draggable, decorators: [ WithWpCompatOverlaySlot ], - parameters: { - sourceLink: - 'storybook/stories/playground/draggable-cross-document-fallback.story.jsx', - }, }; export const InsideIframeWithCompatSlot = () => { diff --git a/storybook/stories/playground/popover-with-slotfill.story.jsx b/storybook/stories/playground/popover-with-slotfill.story.jsx index 96a5d8022e3bf5..8148621f45a052 100644 --- a/storybook/stories/playground/popover-with-slotfill.story.jsx +++ b/storybook/stories/playground/popover-with-slotfill.story.jsx @@ -8,10 +8,6 @@ import { Popover } from '@wordpress/ui'; export default { title: 'Playground/Popover with SlotFill', component: Popover.Root, - parameters: { - sourceLink: - 'storybook/stories/playground/popover-with-slotfill.story.jsx', - }, }; // Renders an iframe and portals `children` into its body once `load` fires. From 479051b800ed3743759d6918a0058567ea1ef240 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 15 May 2026 11:38:01 +0200 Subject: [PATCH 14/27] Draggable: Migrate styles from SCSS to a CSS module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the (already small) Draggable stylesheet to a CSS module so its rules travel via `@wordpress/style-runtime` (and therefore into any iframe wrapped in `` — e.g. the block-editor canvas) without needing the package-level `build-style/style.css` bundle. Drops the `@use` line from `packages/components/src/style.scss`, following the same shape as the `AlignmentMatrixControl` (#73714/#73757) and `AnglePickerControl` (#73786) migrations. The CSS-module class names are standard (hashed). The legacy `components-draggable__*` / `is-dragging-components-draggable` class names are kept by adding them alongside the hashed ones in the JS `classList.add(...)` calls, since several other Gutenberg packages reference them in their own stylesheets (block-editor's `list-view`, `block-tools`, `block-library`'s `navigation` editor, `edit-widgets`' `widget-area` editor) and block-editor runtime JS reads `is-dragging-components-draggable` off `document.body`. Dropping those names would silently break those consumers. Per mirka's review on PR #78183 (CSS-module option for the iframe story); the corresponding Storybook simplification follows in a separate commit. --- .stylelintrc.js | 33 +++++++++++++------ packages/components/src/draggable/index.tsx | 22 ++++++++----- .../{style.scss => style.module.scss} | 18 +++++----- packages/components/src/style.scss | 1 - 4 files changed, 47 insertions(+), 27 deletions(-) rename packages/components/src/draggable/{style.scss => style.module.scss} (55%) diff --git a/.stylelintrc.js b/.stylelintrc.js index fc6f4c2f64f02a..8678a7550b044a 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,3 +1,8 @@ +// CSS stepped value math functions from Baseline 2024 that Stylelint +// (and its SCSS-aware `function-no-unknown` variant) doesn't yet +// recognize. Can likely be dropped on a future Stylelint upgrade. +const CSS_BASELINE_2024_FUNCTIONS = [ 'round', 'rem', 'mod' ]; + /** @type {import('stylelint').Config} */ module.exports = { extends: '@wordpress/stylelint-config/scss-stylistic', @@ -68,16 +73,7 @@ module.exports = { rules: { 'function-no-unknown': [ true, - { - ignoreFunctions: [ - // CSS stepped value math functions in Baseline 2024. - // This rule exception can likely be removed when - // updating to a more recent version of Stylelint. - 'round', - 'rem', - 'mod', - ], - }, + { ignoreFunctions: CSS_BASELINE_2024_FUNCTIONS }, ], 'declaration-property-max-values': { // Prevents left/right values with shorthand property names (unclear for RTL) @@ -135,6 +131,23 @@ module.exports = { ], }, }, + { + // SCSS modules use the Sass-aware `function-no-unknown` variant. + files: [ '**/*.module.scss', 'routes/**/*.scss' ], + rules: { + 'function-no-unknown': null, + 'scss/function-no-unknown': [ + true, + { + ignoreFunctions: [ + ...CSS_BASELINE_2024_FUNCTIONS, + // Sass helpers from `@wordpress/base-styles`. + 'z-index', + ], + }, + ], + }, + }, ], reportDescriptionlessDisables: true, }; diff --git a/packages/components/src/draggable/index.tsx b/packages/components/src/draggable/index.tsx index f6f30599e49586..0b680b07d73085 100644 --- a/packages/components/src/draggable/index.tsx +++ b/packages/components/src/draggable/index.tsx @@ -14,11 +14,17 @@ import { getWpCompatOverlaySlot } from '@wordpress/ui'; * Internal dependencies */ import type { DraggableProps } from './types'; - -const dragImageClass = 'components-draggable__invisible-drag-image'; -const cloneWrapperClass = 'components-draggable__clone'; +import styles from './style.module.scss'; + +// The hardcoded legacy class names are preserved alongside the +// CSS-module hashed ones for backwards compatibility. +const dragImageClasses = [ + styles.invisibleDragImage, + 'components-draggable__invisible-drag-image', +]; +const cloneWrapperClasses = [ styles.clone, 'components-draggable__clone' ]; +const bodyClasses = [ styles.isDragging, 'is-dragging-components-draggable' ]; const clonePadding = 0; -const bodyClass = 'is-dragging-components-draggable'; /** * `Draggable` is a Component that provides a way to set up a cross-browser @@ -124,12 +130,12 @@ export function Draggable( { // right after. event.dataTransfer.setDragImage is not supported yet in // IE, we need to check for its existence first. if ( 'function' === typeof event.dataTransfer.setDragImage ) { - dragImage.classList.add( dragImageClass ); + dragImage.classList.add( ...dragImageClasses ); ownerDocument.body.appendChild( dragImage ); event.dataTransfer.setDragImage( dragImage, 0, 0 ); } - cloneWrapper.classList.add( cloneWrapperClass ); + cloneWrapper.classList.add( ...cloneWrapperClasses ); if ( cloneClassname ) { cloneWrapper.classList.add( cloneClassname ); @@ -218,7 +224,7 @@ export function Draggable( { ownerDocument.addEventListener( 'dragover', throttledDragOver ); // Update cursor to 'grabbing', document wide. - ownerDocument.body.classList.add( bodyClass ); + ownerDocument.body.classList.add( ...bodyClasses ); if ( onDragStart ) { onDragStart( event ); @@ -235,7 +241,7 @@ export function Draggable( { } // Reset cursor. - ownerDocument.body.classList.remove( bodyClass ); + ownerDocument.body.classList.remove( ...bodyClasses ); ownerDocument.removeEventListener( 'dragover', throttledDragOver ); }; diff --git a/packages/components/src/draggable/style.scss b/packages/components/src/draggable/style.module.scss similarity index 55% rename from packages/components/src/draggable/style.scss rename to packages/components/src/draggable/style.module.scss index 07e1f36973ff2f..13057f4d1450fd 100644 --- a/packages/components/src/draggable/style.scss +++ b/packages/components/src/draggable/style.module.scss @@ -1,21 +1,23 @@ @use "@wordpress/base-styles/z-index" as *; -body.is-dragging-components-draggable { - cursor: move;/* Fallback for IE/Edge < 14 */ - cursor: grabbing !important; -} - -.components-draggable__invisible-drag-image { +.invisibleDragImage { position: fixed; - left: -1000px; + inset-inline-start: -1000px; height: 50px; width: 50px; } -.components-draggable__clone { +.clone { position: fixed; padding: 0; // Should match clonePadding variable. background: transparent; pointer-events: none; + // Stacking fallback for when the clone is not routed into the + // `@wordpress/ui` compat overlay slot. z-index: z-index(".components-draggable__clone"); } + +:global(body).isDragging { + cursor: move; /* Fallback for IE/Edge < 14 */ + cursor: grabbing !important; +} diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss index d316d1716f2e90..6d85fd28f7ac60 100644 --- a/packages/components/src/style.scss +++ b/packages/components/src/style.scss @@ -12,7 +12,6 @@ @use "./combobox-control/style.scss" as *; @use "./color-palette/style.scss" as *; @use "./custom-gradient-picker/style.scss" as *; -@use "./draggable/style.scss" as *; @use "./drop-zone/style.scss" as *; @use "./dropdown/style.scss" as *; @use "./dropdown-menu/style.scss" as *; From 888f2efc362a388d314926f149c40ec30cf6b703 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 15 May 2026 12:12:38 +0200 Subject: [PATCH 15/27] Storybook: Simplify cross-document fallback story with StyleProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that Draggable's styles ship as a CSS module routed through `@wordpress/style-runtime`, the cross-document fallback story no longer needs to manually `?inline`-import and inject the whole `components-ltr` SCSS bundle into the iframe's ``. Wrap the portaled iframe content in `` from `@wordpress/components` instead — `StyleProvider` calls `registerDocument()` on the iframe document, and the style registry replays every registered CSS module (Draggable included) into that document. The visible behavior is unchanged: the orange clone still tracks the cursor inside the iframe, demonstrating the cross-document fallback. Per mirka's review on PR #78183. --- ...raggable-cross-document-fallback.story.jsx | 128 +++++++++--------- 1 file changed, 61 insertions(+), 67 deletions(-) diff --git a/storybook/stories/playground/draggable-cross-document-fallback.story.jsx b/storybook/stories/playground/draggable-cross-document-fallback.story.jsx index 3beebf80c8e37d..049a0fb61a43fc 100644 --- a/storybook/stories/playground/draggable-cross-document-fallback.story.jsx +++ b/storybook/stories/playground/draggable-cross-document-fallback.story.jsx @@ -1,7 +1,10 @@ /** * WordPress dependencies */ -import { Draggable } from '@wordpress/components'; +import { + Draggable, + __experimentalStyleProvider as StyleProvider, +} from '@wordpress/components'; import { createPortal, useEffect, useRef, useState } from '@wordpress/element'; import { getWpCompatOverlaySlot } from '@wordpress/ui'; @@ -9,7 +12,6 @@ import { getWpCompatOverlaySlot } from '@wordpress/ui'; * Internal dependencies */ import { WithWpCompatOverlaySlot } from './with-wp-compat-overlay-slot'; -import componentsStyles from '../../package-styles/components-ltr.lazy.scss?inline'; export default { title: 'Playground/Debug fixtures/Draggable cross-document fallback', @@ -19,23 +21,13 @@ export default { export const InsideIframeWithCompatSlot = () => { const iframeRef = useRef( null ); - const [ iframeBody, setIframeBody ] = useState( null ); + const [ iframeDoc, setIframeDoc ] = useState( null ); - const updateIframeBody = () => { - const iframeDoc = iframeRef.current?.contentDocument; - if ( - iframeDoc?.head && - ! iframeDoc.getElementById( 'components-iframe-styles' ) - ) { - const styleEl = iframeDoc.createElement( 'style' ); - styleEl.id = 'components-iframe-styles'; - styleEl.textContent = componentsStyles; - iframeDoc.head.appendChild( styleEl ); - } - setIframeBody( iframeDoc?.body ?? null ); + const updateIframeDoc = () => { + setIframeDoc( iframeRef.current?.contentDocument ?? null ); }; - useEffect( updateIframeBody, [] ); + useEffect( updateIframeDoc, [] ); return (
@@ -59,7 +51,7 @@ export const InsideIframeWithCompatSlot = () => {