diff --git a/src/cdk/drag-drop/drag.spec.ts b/src/cdk/drag-drop/drag.spec.ts index c56a3526b64c..608f88448e1b 100644 --- a/src/cdk/drag-drop/drag.spec.ts +++ b/src/cdk/drag-drop/drag.spec.ts @@ -27,6 +27,7 @@ import {moveItemInArray} from './drag-utils'; import {CdkDropList} from './drop-list'; import {CdkDragHandle} from './drag-handle'; import {CdkDropListGroup} from './drop-list-group'; +import {extendStyles} from './drag-styling'; const ITEM_HEIGHT = 25; const ITEM_WIDTH = 75; @@ -1057,6 +1058,8 @@ describe('CdkDrag', () => { .toBe('ltr', 'Expected preview element to inherit the directionality.'); expect(previewRect.width).toBe(itemRect.width, 'Expected preview width to match element'); expect(previewRect.height).toBe(itemRect.height, 'Expected preview height to match element'); + expect(preview.style.pointerEvents) + .toBe('none', 'Expected pointer events to be disabled on the preview'); dispatchMouseEvent(document, 'mouseup'); fixture.detectChanges(); @@ -2349,6 +2352,46 @@ describe('CdkDrag', () => { expect(Array.from(component.group._items)).toEqual([component.listOne, component.listTwo]); })); + it('should not be able to drop an element into a container that is under another element', + fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + fixture.detectChanges(); + + const groups = fixture.componentInstance.groupedDragItems.slice(); + const element = groups[0][1].element.nativeElement; + const dropInstances = fixture.componentInstance.dropInstances.toArray(); + const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); + const coverElement = document.createElement('div'); + const targetGroupRect = dropInstances[1].element.nativeElement.getBoundingClientRect(); + + // Add an extra element that covers the target container. + fixture.nativeElement.appendChild(coverElement); + extendStyles(coverElement.style, { + position: 'fixed', + top: targetGroupRect.top + 'px', + left: targetGroupRect.left + 'px', + bottom: targetGroupRect.bottom + 'px', + right: targetGroupRect.right + 'px', + background: 'orange' + }); + + dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1); + flush(); + fixture.detectChanges(); + + const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + + expect(event).toBeTruthy(); + expect(event).toEqual({ + previousIndex: 1, + currentIndex: 1, + item: groups[0][1], + container: dropInstances[0], + previousContainer: dropInstances[0], + isPointerOverContainer: false + }); + })); + }); }); diff --git a/src/cdk/drag-drop/drag.ts b/src/cdk/drag-drop/drag.ts index a59106226100..84ec49590d33 100644 --- a/src/cdk/drag-drop/drag.ts +++ b/src/cdk/drag-drop/drag.ts @@ -672,6 +672,9 @@ export class CdkDrag implements AfterViewInit, OnDestroy { } extendStyles(preview.style, { + // It's important that we disable the pointer events on the preview, because + // it can throw off the `document.elementFromPoint` calls in the `CdkDropList`. + pointerEvents: 'none', position: 'fixed', top: '0', left: '0', diff --git a/src/cdk/drag-drop/drop-list.ts b/src/cdk/drag-drop/drop-list.ts index 0c73e0a85778..5641ee6dbc69 100644 --- a/src/cdk/drag-drop/drop-list.ts +++ b/src/cdk/drag-drop/drop-list.ts @@ -21,8 +21,10 @@ import { Directive, ChangeDetectorRef, SkipSelf, + Inject, } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; +import {DOCUMENT} from '@angular/common'; import {CdkDrag} from './drag'; import {DragDropRegistry} from './drag-drop-registry'; import {CdkDragDrop, CdkDragEnter, CdkDragExit, CdkDragSortEvent} from './drag-events'; @@ -93,6 +95,8 @@ interface ListPositionCacheEntry { } }) export class CdkDropList implements OnInit, OnDestroy { + private _document: Document | undefined; + /** Draggable items in the container. */ @ContentChildren(forwardRef(() => CdkDrag)) _draggables: QueryList; @@ -160,7 +164,17 @@ export class CdkDropList implements OnInit, OnDestroy { private _dragDropRegistry: DragDropRegistry>, private _changeDetectorRef: ChangeDetectorRef, @Optional() private _dir?: Directionality, - @Optional() @SkipSelf() private _group?: CdkDropListGroup) {} + @Optional() @SkipSelf() private _group?: CdkDropListGroup, + // @breaking-change 8.0.0 `_document` parameter to be made required. + @Optional() @Inject(DOCUMENT) _document?: any) { + + // @breaking-change 8.0.0 Remove null checks once `_document` parameter is required. + if (_document) { + this._document = _document; + } else if (typeof document !== 'undefined') { + this._document = document; + } + } ngOnInit() { this._dragDropRegistry.registerDropContainer(this); @@ -384,8 +398,39 @@ export class CdkDropList implements OnInit, OnDestroy { * @param y Position of the item along the Y axis. */ _getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropList | null { - const result = this._positionCache.siblings - .find(sibling => isInsideClientRect(sibling.clientRect, x, y)); + const results = this._positionCache.siblings.filter(sibling => { + return isInsideClientRect(sibling.clientRect, x, y); + }); + + // No drop containers are intersecting with the pointer. + if (!results.length) { + return null; + } + + let result: ListPositionCacheEntry | undefined = results[0]; + + // @breaking-change 8.0.0 remove null check once the + // `_document` is made into a required parameter. + if (this._document) { + const elementFromPoint = this._document.elementFromPoint(x, y); + + // If there's no element at the pointer position, then + // the client rect is probably scrolled out of the view. + if (!elementFromPoint) { + return null; + } + + // The `ClientRect`, that we're using to find the container over which the user is + // hovering, doesn't give us any information on whether the element has been scrolled + // out of the view or whether it's overlapping with other containers. This means that + // we could end up transferring the item into a container that's invisible or is positioned + // below another one. We use the result from `elementFromPoint` to get the top-most element + // at the pointer position and to find whether it's one of the intersecting drop containers. + result = results.find(sibling => { + const element = sibling.drop.element.nativeElement; + return element === elementFromPoint || element.contains(elementFromPoint); + }); + } return result && result.drop.enterPredicate(item, result.drop) ? result.drop : null; } diff --git a/tools/public_api_guard/cdk/drag-drop.d.ts b/tools/public_api_guard/cdk/drag-drop.d.ts index 8790915aeb48..4d104fc6af89 100644 --- a/tools/public_api_guard/cdk/drag-drop.d.ts +++ b/tools/public_api_guard/cdk/drag-drop.d.ts @@ -120,7 +120,7 @@ export declare class CdkDropList implements OnInit, OnDestroy { lockAxis: 'x' | 'y'; orientation: 'horizontal' | 'vertical'; sorted: EventEmitter>; - constructor(element: ElementRef, _dragDropRegistry: DragDropRegistry>, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup> | undefined); + constructor(element: ElementRef, _dragDropRegistry: DragDropRegistry>, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup> | undefined, _document?: any); _getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropList | null; _isOverContainer(x: number, y: number): boolean; _sortItem(item: CdkDrag, pointerX: number, pointerY: number, pointerDelta: {