Skip to content

Commit 5e5f87a

Browse files
authored
fix(cdk/drag-drop): preview not inheriting styles inside shadow dom (#21107)
By default we create a drag preview by cloning the element and inserting the preview at the document root to avoid issues with `overflow: hidden`. The problem is that the clone won't look correctly if the host styles were encapsulated to the shadow DOM. These changes make it so that we insert the preview at the closest shadow root if the host is inside the shadow DOM, otherwise we fall back to the `body`.
1 parent e8ae494 commit 5e5f87a

File tree

2 files changed

+47
-4
lines changed

2 files changed

+47
-4
lines changed

src/cdk/drag-drop/directives/drag.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5464,6 +5464,24 @@ describe('CdkDrag', () => {
54645464
.toBe(1, 'Expected only one item to continue to be dragged.');
54655465
}));
54665466

5467+
it('should insert the preview inside the shadow root by default', fakeAsync(() => {
5468+
// This test is only relevant for Shadow DOM-supporting browsers.
5469+
if (!_supportsShadowDom()) {
5470+
return;
5471+
}
5472+
5473+
const fixture = createComponent(ConnectedDropZonesInsideShadowRoot);
5474+
fixture.detectChanges();
5475+
const item = fixture.componentInstance.groupedDragItems[0][1];
5476+
5477+
startDraggingViaMouse(fixture, item.element.nativeElement);
5478+
fixture.detectChanges();
5479+
5480+
// `querySelector` doesn't descend into the shadow DOM so we can assert that the preview
5481+
// isn't at its default location by searching for it at the `document` level.
5482+
expect(document.querySelector('.cdk-drag-preview')).toBeFalsy();
5483+
}));
5484+
54675485
});
54685486

54695487
describe('with nested drags', () => {

src/cdk/drag-drop/drag-ref.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {EmbeddedViewRef, ElementRef, NgZone, ViewContainerRef, TemplateRef} from '@angular/core';
1010
import {ViewportRuler} from '@angular/cdk/scrolling';
1111
import {Direction} from '@angular/cdk/bidi';
12-
import {normalizePassiveListenerOptions} from '@angular/cdk/platform';
12+
import {normalizePassiveListenerOptions, _getShadowRoot} from '@angular/cdk/platform';
1313
import {coerceBooleanProperty, coerceElement} from '@angular/cdk/coercion';
1414
import {Subscription, Subject, Observable} from 'rxjs';
1515
import {DropListRefInternal as DropListRef} from './drop-list-ref';
@@ -227,6 +227,13 @@ export class DragRef<T = any> {
227227
/** Layout direction of the item. */
228228
private _direction: Direction = 'ltr';
229229

230+
/**
231+
* Cached shadow root that the element is placed in. `null` means that the element isn't in
232+
* the shadow DOM and `undefined` means that it hasn't been resolved yet. Should be read via
233+
* `_getShadowRoot`, not directly.
234+
*/
235+
private _cachedShadowRoot: ShadowRoot | null | undefined;
236+
230237
/** Axis along which dragging is locked. */
231238
lockAxis: 'x' | 'y';
232239

@@ -743,6 +750,9 @@ export class DragRef<T = any> {
743750
const placeholder = this._placeholder = this._createPlaceholderElement();
744751
const anchor = this._anchor = this._anchor || this._document.createComment('');
745752

753+
// Needs to happen before the root element is moved.
754+
const shadowRoot = this._getShadowRoot();
755+
746756
// Insert an anchor node so that we can restore the element's position in the DOM.
747757
parent.insertBefore(anchor, element);
748758

@@ -751,7 +761,7 @@ export class DragRef<T = any> {
751761
// from the DOM completely, because iOS will stop firing all subsequent events in the chain.
752762
toggleVisibility(element, false);
753763
this._document.body.appendChild(parent.replaceChild(placeholder, element));
754-
getPreviewInsertionPoint(this._document).appendChild(preview);
764+
getPreviewInsertionPoint(this._document, shadowRoot).appendChild(preview);
755765
this.started.next({source: this}); // Emit before notifying the container.
756766
dropContainer.start();
757767
this._initialContainer = dropContainer;
@@ -1319,6 +1329,20 @@ export class DragRef<T = any> {
13191329
return cachedPosition ? cachedPosition.scrollPosition :
13201330
this._viewportRuler.getViewportScrollPosition();
13211331
}
1332+
1333+
/**
1334+
* Lazily resolves and returns the shadow root of the element. We do this in a function, rather
1335+
* than saving it in property directly on init, because we want to resolve it as late as possible
1336+
* in order to ensure that the element has been moved into the shadow DOM. Doing it inside the
1337+
* constructor might be too early if the element is inside of something like `ngFor` or `ngIf`.
1338+
*/
1339+
private _getShadowRoot(): ShadowRoot | null {
1340+
if (this._cachedShadowRoot === undefined) {
1341+
this._cachedShadowRoot = _getShadowRoot(this._rootElement) as ShadowRoot | null;
1342+
}
1343+
1344+
return this._cachedShadowRoot;
1345+
}
13221346
}
13231347

13241348
/**
@@ -1356,11 +1380,12 @@ function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
13561380
}
13571381

13581382
/** Gets the element into which the drag preview should be inserted. */
1359-
function getPreviewInsertionPoint(documentRef: any): HTMLElement {
1383+
function getPreviewInsertionPoint(documentRef: any, shadowRoot: ShadowRoot | null): HTMLElement {
13601384
// We can't use the body if the user is in fullscreen mode,
13611385
// because the preview will render under the fullscreen element.
13621386
// TODO(crisbeto): dedupe this with the `FullscreenOverlayContainer` eventually.
1363-
return documentRef.fullscreenElement ||
1387+
return shadowRoot ||
1388+
documentRef.fullscreenElement ||
13641389
documentRef.webkitFullscreenElement ||
13651390
documentRef.mozFullScreenElement ||
13661391
documentRef.msFullscreenElement ||

0 commit comments

Comments
 (0)