From 150bf29da9a6f7396108f026bbaf9b3e6e28c8c0 Mon Sep 17 00:00:00 2001 From: Guillaume Fauvet Date: Thu, 27 Aug 2026 16:50:35 +0200 Subject: [PATCH] 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;