From d31058b1978d55e10a721a0fd86ecde8f8a321af Mon Sep 17 00:00:00 2001 From: Guillaume Fauvet Date: Thu, 27 Aug 2026 16:28:22 +0200 Subject: [PATCH 1/2] fix(sorter): keep the placeholder attached when the editor is in a shadow root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sorter#ensurePlaceholderElement re-appended the placeholder to containerContext.container whenever `!el.ownerDocument.contains(el)`. `Node.contains` walks the node tree without crossing shadow boundaries, so for an editor mounted in a shadow root the guard always answered "not attached" — the placeholder element is `Canvas#getPlacerEl()`, which lives in the tools layer, i.e. in the host page DOM. Every placeholder move therefore ripped it out of the tools layer and injected it into the container, which for the canvas sorters is the iframe body (Droppable, SelectPosition and BlocksView all pair `getPlacerEl()` with the canvas body). The GrapesJS stylesheet is not loaded there, so the placeholder became invisible. Fix: test `el.isConnected`, the shadow-aware equivalent, which behaves identically outside shadow DOM. Introduced by the sorter refactor in #6149 (0.22.1). The previous implementation only appended the placeholder when the `placer` option was absent, and the canvas sorters always passed it, so the append branch was never taken. --- packages/core/src/utils/sorter/Sorter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/utils/sorter/Sorter.ts b/packages/core/src/utils/sorter/Sorter.ts index 78897968f..a366860bf 100644 --- a/packages/core/src/utils/sorter/Sorter.ts +++ b/packages/core/src/utils/sorter/Sorter.ts @@ -173,7 +173,7 @@ export default class Sorter> { private ensurePlaceholderElement() { const el = this.placeholder.el; const container = this.containerContext.container; - if (!el.ownerDocument.contains(el)) { + if (!el.isConnected) { container.append(this.placeholder.el); } } From 150bf29da9a6f7396108f026bbaf9b3e6e28c8c0 Mon Sep 17 00:00:00 2001 From: Guillaume Fauvet Date: Thu, 27 Aug 2026 16:50:35 +0200 Subject: [PATCH 2/2] fix(sorter): hit-test against the container's root node so sorting works in a shadow root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DropLocationDeterminer#getMouseTargetElement resolved the drop target with `containerContext.document.elementFromPoint()`. `Document.elementFromPoint` does not pierce shadow boundaries, so when a sorter container lives inside a shadow root it returned the shadow host instead of the hovered item. The subsequent `getFirstElementWithAModel` walk is bounded by `containerContext.container.contains(el)`, which is false for the host, so it returned null and handleMove bailed out on every sample: no placeholder, no drop, silent failure. This hits the Layer Manager when rendered into the host page via `layerManager.appendTo` (ItemsView hardcodes `document: document`), making layer reordering a no-op. The canvas is unaffected because its containerContext document is the iframe document. Fix: run the hit-test on `container.getRootNode()`, which is the ShadowRoot when the container is in a shadow tree and the Document otherwise. `ShadowRoot` implements the same DocumentOrShadowRoot mixin and resolves within its own tree, so behaviour outside shadow DOM is unchanged; the isFunction guard falls back to containerContext.document for a detached container. Introduced by the sorter refactor in #6149 (0.22.1), which replaced the previous `e.target` lookup — not retargeted for a listener inside the same shadow tree. Co-Authored-By: Claude Opus 5 (1M context) --- .../utils/sorter/DropLocationDeterminer.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/core/src/utils/sorter/DropLocationDeterminer.ts b/packages/core/src/utils/sorter/DropLocationDeterminer.ts index d65e41fa3..893fa5f5a 100644 --- a/packages/core/src/utils/sorter/DropLocationDeterminer.ts +++ b/packages/core/src/utils/sorter/DropLocationDeterminer.ts @@ -4,7 +4,7 @@ import EditorModel from '../../editor/model/Editor'; import { isTextNode, off, on } from '../dom'; import { SortableTreeNode } from './SortableTreeNode'; import { Placement, PositionOptions, DragDirection, SorterEventHandlers, CustomTarget, DragSource } from './types'; -import { bindAll, each } from 'underscore'; +import { bindAll, each, isFunction } from 'underscore'; import { matches, findPosition, offset, isStyleInFlow } from './SorterUtils'; import { RateLimiter } from './RateLimiter'; import Dimension from './Dimension'; @@ -287,9 +287,24 @@ export class DropLocationDeterminer> ext return newHoveredNode; } + /** + * Resolves the root to run the hit-test against. + * + * `Document.elementFromPoint` doesn't pierce shadow boundaries, so when the container lives + * inside a shadow root it would always return the host element instead of the hovered item. + * `ShadowRoot` implements the same `DocumentOrShadowRoot` mixin and resolves within its own tree. + */ + private getContainerContextRoot(): DocumentOrShadowRoot { + const root = this.containerContext.container?.getRootNode() as Partial | undefined; + if (isFunction(root?.elementFromPoint)) { + return root as DocumentOrShadowRoot; + } + return this.containerContext.document; + } + private getMouseTargetElement(mouseEvent: MouseEvent) { const customTarget = this.containerContext.customTarget; - let mouseTarget = this.containerContext.document.elementFromPoint( + let mouseTarget = this.getContainerContextRoot().elementFromPoint( mouseEvent.clientX, mouseEvent.clientY, ) as HTMLElement;