From b6bd3d6173c8dabcf1e6c37160e82cfb8d0ce3fa Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sat, 31 Aug 2019 10:15:18 +0200 Subject: [PATCH] fix(drag-drop): connected drop zones not working inside shadow root Fixes not being able to drop into a connected drop list when using `ShadowDom` view encapsulation. The issue comes from the fact that we use `elementFromPoint` to figure out whether the user's pointer is over a drop list. When the element is inside a shadow root, calling `elementFromPoint` on the `document` will return the shadow root. These changes fix the issue by calling `elementFromPoint` from the shadow root instead. Fixes #16898. --- src/cdk/drag-drop/BUILD.bazel | 1 + src/cdk/drag-drop/directives/drag.spec.ts | 150 ++++++++++++++-------- src/cdk/drag-drop/drop-list-ref.ts | 25 +++- 3 files changed, 119 insertions(+), 57 deletions(-) diff --git a/src/cdk/drag-drop/BUILD.bazel b/src/cdk/drag-drop/BUILD.bazel index 1285650897d1..ab8d31e89f64 100644 --- a/src/cdk/drag-drop/BUILD.bazel +++ b/src/cdk/drag-drop/BUILD.bazel @@ -35,6 +35,7 @@ ng_test_library( deps = [ ":drag-drop", "//src/cdk/bidi", + "//src/cdk/platform", "//src/cdk/scrolling", "//src/cdk/testing", "@npm//@angular/common", diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 6ff16c501be4..3a97cf834fb1 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -24,6 +24,7 @@ import { import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing'; import {DOCUMENT} from '@angular/common'; import {ViewportRuler} from '@angular/cdk/scrolling'; +import {_supportsShadowDom} from '@angular/cdk/platform'; import {of as observableOf} from 'rxjs'; import {DragDropModule} from '../drag-drop-module'; @@ -4010,6 +4011,39 @@ describe('CdkDrag', () => { cleanup(); })); + it('should be able to drop into a new container inside the Shadow DOM', fakeAsync(() => { + // This test is only relevant for Shadow DOM-supporting browsers. + if (!_supportsShadowDom()) { + return; + } + + const fixture = createComponent(ConnectedDropZonesInsideShadowRoot); + fixture.detectChanges(); + + const groups = fixture.componentInstance.groupedDragItems; + const item = groups[0][1]; + const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); + + dragElementViaMouse(fixture, item.element.nativeElement, + targetRect.left + 1, targetRect.top + 1); + flush(); + fixture.detectChanges(); + + expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); + + const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + + expect(event).toEqual({ + previousIndex: 1, + currentIndex: 3, + item, + container: fixture.componentInstance.dropInstances.toArray()[1], + previousContainer: fixture.componentInstance.dropInstances.first, + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} + }); + })); + }); describe('with nested drags', () => { @@ -4389,65 +4423,68 @@ class DraggableInDropZoneWithCustomPlaceholder { renderPlaceholder = true; } +const CONNECTED_DROP_ZONES_STYLES = [` + .cdk-drop-list { + display: block; + width: 100px; + min-height: ${ITEM_HEIGHT}px; + background: hotpink; + } -@Component({ - encapsulation: ViewEncapsulation.None, - styles: [` - .cdk-drop-list { - display: block; - width: 100px; - min-height: ${ITEM_HEIGHT}px; - background: hotpink; - } + .cdk-drag { + display: block; + height: ${ITEM_HEIGHT}px; + background: red; + } +`]; - .cdk-drag { - display: block; - height: ${ITEM_HEIGHT}px; - background: red; - } - `], - template: ` +const CONNECTED_DROP_ZONES_TEMPLATE = ` +
-
{{item}}
-
+ [cdkDragData]="item" + (cdkDragEntered)="itemEnteredSpy($event)" + *ngFor="let item of todo" + cdkDrag>{{item}}
+ +
-
{{item}}
-
+ [cdkDragData]="item" + (cdkDragEntered)="itemEnteredSpy($event)" + *ngFor="let item of done" + cdkDrag>{{item}}
+ +
-
{{item}}
-
- ` + [cdkDragData]="item" + (cdkDragEntered)="itemEnteredSpy($event)" + *ngFor="let item of extra" + cdkDrag>{{item}}
+ +`; + +@Component({ + encapsulation: ViewEncapsulation.None, + styles: CONNECTED_DROP_ZONES_STYLES, + template: CONNECTED_DROP_ZONES_TEMPLATE }) class ConnectedDropZones implements AfterViewInit { @ViewChildren(CdkDrag) rawDragItems: QueryList; @@ -4472,6 +4509,15 @@ class ConnectedDropZones implements AfterViewInit { } } +@Component({ + encapsulation: ViewEncapsulation.ShadowDom, + styles: CONNECTED_DROP_ZONES_STYLES, + template: CONNECTED_DROP_ZONES_TEMPLATE +}) +class ConnectedDropZonesInsideShadowRoot extends ConnectedDropZones { +} + + @Component({ encapsulation: ViewEncapsulation.None, styles: [` diff --git a/src/cdk/drag-drop/drop-list-ref.ts b/src/cdk/drag-drop/drop-list-ref.ts index 8ea82d51c65f..70248bce7bd7 100644 --- a/src/cdk/drag-drop/drop-list-ref.ts +++ b/src/cdk/drag-drop/drop-list-ref.ts @@ -10,6 +10,7 @@ import {ElementRef, NgZone} from '@angular/core'; import {Direction} from '@angular/cdk/bidi'; import {coerceElement} from '@angular/cdk/coercion'; import {ViewportRuler} from '@angular/cdk/scrolling'; +import {_supportsShadowDom} from '@angular/cdk/platform'; import {Subject, Subscription, interval, animationFrameScheduler} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {moveItemInArray} from './drag-utils'; @@ -74,8 +75,6 @@ export interface DropListRefInternal extends DropListRef {} * @docs-private */ export class DropListRef { - private _document: Document; - /** Element that the drop list is attached to. */ element: HTMLElement | ElementRef; @@ -201,6 +200,9 @@ export class DropListRef { /** Used to signal to the current auto-scroll sequence when to stop. */ private _stopScrollTimers = new Subject(); + /** Shadow root of the current element. Necessary for `elementFromPoint` to resolve correctly. */ + private _shadowRoot: DocumentOrShadowRoot; + constructor( element: ElementRef | HTMLElement, private _dragDropRegistry: DragDropRegistry, @@ -211,9 +213,9 @@ export class DropListRef { */ private _ngZone?: NgZone, private _viewportRuler?: ViewportRuler) { + const nativeNode = this.element = coerceElement(element); + this._shadowRoot = getShadowRoot(nativeNode) || _document; _dragDropRegistry.registerDropContainer(this); - this._document = _document; - this.element = element instanceof ElementRef ? element.nativeElement : element; } /** Removes the drop list functionality from the DOM element. */ @@ -815,7 +817,7 @@ export class DropListRef { return false; } - const elementFromPoint = this._document.elementFromPoint(x, y) as HTMLElement | null; + const elementFromPoint = this._shadowRoot.elementFromPoint(x, y) as HTMLElement | null; // If there's no element at the pointer position, then // the client rect is probably scrolled out of the view. @@ -1049,3 +1051,16 @@ function getElementScrollDirections(element: HTMLElement, clientRect: ClientRect return [verticalScrollDirection, horizontalScrollDirection]; } + +/** Gets the shadow root of an element, if any. */ +function getShadowRoot(element: HTMLElement): DocumentOrShadowRoot | null { + if (_supportsShadowDom()) { + const rootNode = element.getRootNode ? element.getRootNode() : null; + + if (rootNode instanceof ShadowRoot) { + return rootNode; + } + } + + return null; +}