diff --git a/src/cdk/drag-drop/drag-events.ts b/src/cdk/drag-drop/drag-events.ts index f1e1ffb23140..be00099243be 100644 --- a/src/cdk/drag-drop/drag-events.ts +++ b/src/cdk/drag-drop/drag-events.ts @@ -53,6 +53,8 @@ export interface CdkDragDrop { container: CdkDropListContainer; /** Container from which the item was picked up. Can be the same as the `container`. */ previousContainer: CdkDropListContainer; + /** Whether the user's pointer was over the container when the item was dropped. */ + isPointerOverContainer: boolean; } /** Event emitted as the user is dragging a draggable item. */ diff --git a/src/cdk/drag-drop/drag.spec.ts b/src/cdk/drag-drop/drag.spec.ts index a37613983cbd..41528a2895bf 100644 --- a/src/cdk/drag-drop/drag.spec.ts +++ b/src/cdk/drag-drop/drag.spec.ts @@ -765,13 +765,55 @@ describe('CdkDrag', () => { currentIndex: 2, item: firstItem, container: fixture.componentInstance.dropInstance, - previousContainer: fixture.componentInstance.dropInstance + previousContainer: fixture.componentInstance.dropInstance, + isPointerOverContainer: true }); expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) .toEqual(['One', 'Two', 'Zero', 'Three']); })); + it('should expose whether an item was dropped over a container', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + const dragItems = fixture.componentInstance.dragItems; + const firstItem = dragItems.first; + const thirdItemRect = dragItems.toArray()[2].element.nativeElement.getBoundingClientRect(); + + dragElementViaMouse(fixture, firstItem.element.nativeElement, + thirdItemRect.left + 1, thirdItemRect.top + 1); + flush(); + fixture.detectChanges(); + + expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); + + const event: CdkDragDrop = + fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + + expect(event.isPointerOverContainer).toBe(true); + })); + + it('should expose whether an item was dropped outside of a container', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + const dragItems = fixture.componentInstance.dragItems; + const firstItem = dragItems.first; + const containerRect = fixture.componentInstance.dropInstance.element + .nativeElement.getBoundingClientRect(); + + dragElementViaMouse(fixture, firstItem.element.nativeElement, + containerRect.right + 10, containerRect.bottom + 10); + flush(); + fixture.detectChanges(); + + expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); + + const event: CdkDragDrop = + fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + + expect(event.isPointerOverContainer).toBe(false); + })); + it('should dispatch the `sorted` event as an item is being sorted', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); @@ -830,7 +872,8 @@ describe('CdkDrag', () => { currentIndex: 0, item: firstItem, container: fixture.componentInstance.dropInstance, - previousContainer: fixture.componentInstance.dropInstance + previousContainer: fixture.componentInstance.dropInstance, + isPointerOverContainer: false }); expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) @@ -887,7 +930,8 @@ describe('CdkDrag', () => { currentIndex: 2, item: firstItem, container: fixture.componentInstance.dropInstance, - previousContainer: fixture.componentInstance.dropInstance + previousContainer: fixture.componentInstance.dropInstance, + isPointerOverContainer: true }); expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) @@ -926,7 +970,8 @@ describe('CdkDrag', () => { currentIndex: 2, item: firstItem, container: fixture.componentInstance.dropInstance, - previousContainer: fixture.componentInstance.dropInstance + previousContainer: fixture.componentInstance.dropInstance, + isPointerOverContainer: true }); expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) @@ -961,7 +1006,8 @@ describe('CdkDrag', () => { currentIndex: 0, item: firstItem, container: fixture.componentInstance.dropInstance, - previousContainer: fixture.componentInstance.dropInstance + previousContainer: fixture.componentInstance.dropInstance, + isPointerOverContainer: false }); expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) @@ -1797,7 +1843,8 @@ describe('CdkDrag', () => { currentIndex: 3, item, container: fixture.componentInstance.dropInstances.toArray()[1], - previousContainer: fixture.componentInstance.dropInstances.first + previousContainer: fixture.componentInstance.dropInstances.first, + isPointerOverContainer: true }); })); @@ -1898,7 +1945,8 @@ describe('CdkDrag', () => { currentIndex: 3, item: groups[0][1], container: dropInstances[1], - previousContainer: dropInstances[0] + previousContainer: dropInstances[0], + isPointerOverContainer: true }); })); @@ -1927,7 +1975,8 @@ describe('CdkDrag', () => { currentIndex: 1, item: groups[0][1], container: dropInstances[0], - previousContainer: dropInstances[0] + previousContainer: dropInstances[0], + isPointerOverContainer: false }); })); @@ -1956,7 +2005,8 @@ describe('CdkDrag', () => { currentIndex: 1, item: groups[0][1], container: dropInstances[0], - previousContainer: dropInstances[0] + previousContainer: dropInstances[0], + isPointerOverContainer: false }); })); @@ -2078,7 +2128,8 @@ describe('CdkDrag', () => { currentIndex: 3, item: groups[0][1], container: dropInstances[1], - previousContainer: dropInstances[0] + previousContainer: dropInstances[0], + isPointerOverContainer: true }); })); @@ -2103,7 +2154,8 @@ describe('CdkDrag', () => { currentIndex: 3, item: groups[0][1], container: dropInstances[1], - previousContainer: dropInstances[0] + previousContainer: dropInstances[0], + isPointerOverContainer: true }); })); @@ -2133,7 +2185,8 @@ describe('CdkDrag', () => { currentIndex: 3, item: groups[0][1], container: dropInstances[1], - previousContainer: dropInstances[0] + previousContainer: dropInstances[0], + isPointerOverContainer: true }); })); @@ -2167,7 +2220,8 @@ describe('CdkDrag', () => { currentIndex: 0, item, container: fixture.componentInstance.dropInstances.toArray()[1], - previousContainer: fixture.componentInstance.dropInstances.first + previousContainer: fixture.componentInstance.dropInstances.first, + isPointerOverContainer: true }); expect(dropContainers[0].contains(item.element.nativeElement)).toBe(true, @@ -2656,7 +2710,7 @@ function dragElementViaMouse(fixture: ComponentFixture, dispatchMouseEvent(document, 'mousemove', x, y); fixture.detectChanges(); - dispatchMouseEvent(document, 'mouseup'); + dispatchMouseEvent(document, 'mouseup', x, y); fixture.detectChanges(); } @@ -2695,7 +2749,7 @@ function dragElementViaTouch(fixture: ComponentFixture, dispatchTouchEvent(document, 'touchmove', x, y); fixture.detectChanges(); - dispatchTouchEvent(document, 'touchend'); + dispatchTouchEvent(document, 'touchend', x, y); fixture.detectChanges(); } diff --git a/src/cdk/drag-drop/drag.ts b/src/cdk/drag-drop/drag.ts index c852a91a3970..33d4795805d6 100644 --- a/src/cdk/drag-drop/drag.ts +++ b/src/cdk/drag-drop/drag.ts @@ -515,7 +515,7 @@ export class CdkDrag implements AfterViewInit, OnDestroy { } /** Handler that is invoked when the user lifts their pointer up, after initiating a drag. */ - private _pointerUp = () => { + private _pointerUp = (event: MouseEvent | TouchEvent) => { if (!this._isDragging()) { return; } @@ -539,13 +539,13 @@ export class CdkDrag implements AfterViewInit, OnDestroy { } this._animatePreviewToPlaceholder().then(() => { - this._cleanupDragArtifacts(); + this._cleanupDragArtifacts(event); this._dragDropRegistry.stopDragging(this); }); } /** Cleans up the DOM artifacts that were added to facilitate the element being dragged. */ - private _cleanupDragArtifacts() { + private _cleanupDragArtifacts(event: MouseEvent | TouchEvent) { // Restore the element's visibility and insert it at its old position in the DOM. // It's important that we maintain the position, because moving the element around in the DOM // can throw off `NgFor` which does smart diffing and re-creates elements only when necessary, @@ -564,6 +564,8 @@ export class CdkDrag implements AfterViewInit, OnDestroy { // Re-enter the NgZone since we bound `document` events on the outside. this._ngZone.run(() => { const currentIndex = this.dropContainer.getItemIndex(this); + const {x, y} = this._getPointerPositionOnPage(event); + const isPointerOverContainer = this.dropContainer._isOverContainer(x, y); this.ended.emit({source: this}); this.dropped.emit({ @@ -571,9 +573,10 @@ export class CdkDrag implements AfterViewInit, OnDestroy { currentIndex, previousIndex: this._initialContainer.getItemIndex(this), container: this.dropContainer, - previousContainer: this._initialContainer + previousContainer: this._initialContainer, + isPointerOverContainer }); - this.dropContainer.drop(this, currentIndex, this._initialContainer); + this.dropContainer.drop(this, currentIndex, this._initialContainer, isPointerOverContainer); this.dropContainer = this._initialContainer; }); } @@ -587,11 +590,11 @@ export class CdkDrag implements AfterViewInit, OnDestroy { let newContainer = this.dropContainer._getSiblingContainerFromPosition(this, x, y); // If we couldn't find a new container to move the item into, and the item has left it's - // initial container, check whether the it's allowed to return into its original container. - // This handles the case where two containers are connected one way and the user tries to - // undo dragging an item into a new container. + // initial container, check whether the it's over the initial container. This handles the + // case where two containers are connected one way and the user tries to undo dragging an + // item into a new container. if (!newContainer && this.dropContainer !== this._initialContainer && - this._initialContainer._canReturnItem(x, y)) { + this._initialContainer._isOverContainer(x, y)) { newContainer = this._initialContainer; } diff --git a/src/cdk/drag-drop/drop-list-container.ts b/src/cdk/drag-drop/drop-list-container.ts index 296438017536..7774c37cbcaa 100644 --- a/src/cdk/drag-drop/drop-list-container.ts +++ b/src/cdk/drag-drop/drop-list-container.ts @@ -37,8 +37,11 @@ export interface CdkDropListContainer { * @param item Item being dropped into the container. * @param currentIndex Index at which the item should be inserted. * @param previousContainer Container from which the item got dragged in. + * @param isPointerOverContainer Whether the user's pointer was over the + * container when the item was dropped. */ - drop(item: CdkDrag, currentIndex: number, previousContainer?: CdkDropListContainer): void; + drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropListContainer, + isPointerOverContainer: boolean): void; /** * Emits an event to indicate that the user moved an item into the container. @@ -63,7 +66,7 @@ export interface CdkDropListContainer { _draggables: QueryList; _getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropListContainer | null; - _canReturnItem(x: number, y: number): boolean; + _isOverContainer(x: number, y: number): boolean; } /** diff --git a/src/cdk/drag-drop/drop-list.ts b/src/cdk/drag-drop/drop-list.ts index 00ab3c667afe..b36ccb35bf18 100644 --- a/src/cdk/drag-drop/drop-list.ts +++ b/src/cdk/drag-drop/drop-list.ts @@ -210,16 +210,19 @@ export class CdkDropList implements OnInit, OnDestroy { * @param item Item being dropped into the container. * @param currentIndex Index at which the item should be inserted. * @param previousContainer Container from which the item got dragged in. + * @param isPointerOverContainer Whether the user's pointer was over the + * container when the item was dropped. */ - drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropList): void { + drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropList, + isPointerOverContainer: boolean): void { this._reset(); this.dropped.emit({ item, currentIndex, previousIndex: previousContainer.getItemIndex(item), container: this, - // TODO(crisbeto): reconsider whether to make this null if the containers are the same. - previousContainer + previousContainer, + isPointerOverContainer }); } @@ -386,12 +389,11 @@ export class CdkDropList implements OnInit, OnDestroy { } /** - * Checks whether an item that started in this container can be returned to it, - * after it was moved out into another container. - * @param x Position of the item along the X axis. - * @param y Position of the item along the Y axis. + * Checks whether the user's pointer is positioned over the container. + * @param x Pointer position along the X axis. + * @param y Pointer position along the Y axis. */ - _canReturnItem(x: number, y: number): boolean { + _isOverContainer(x: number, y: number): boolean { return isInsideClientRect(this._positionCache.self, x, y); } diff --git a/tools/public_api_guard/cdk/drag-drop.d.ts b/tools/public_api_guard/cdk/drag-drop.d.ts index 6fb9b60ecbde..011421f7c27b 100644 --- a/tools/public_api_guard/cdk/drag-drop.d.ts +++ b/tools/public_api_guard/cdk/drag-drop.d.ts @@ -41,6 +41,7 @@ export interface CdkDragConfig { export interface CdkDragDrop { container: CdkDropListContainer; currentIndex: number; + isPointerOverContainer: boolean; item: CdkDrag; previousContainer: CdkDropListContainer; previousIndex: number; @@ -119,13 +120,13 @@ export declare class CdkDropList implements OnInit, OnDestroy { orientation: 'horizontal' | 'vertical'; sorted: EventEmitter>; constructor(element: ElementRef, _dragDropRegistry: DragDropRegistry>, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup> | undefined); - _canReturnItem(x: number, y: number): boolean; _getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropList | null; + _isOverContainer(x: number, y: number): boolean; _sortItem(item: CdkDrag, pointerX: number, pointerY: number, pointerDelta: { x: number; y: number; }): void; - drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropList): void; + drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropList, isPointerOverContainer: boolean): void; enter(item: CdkDrag, pointerX: number, pointerY: number): void; exit(item: CdkDrag): void; getItemIndex(item: CdkDrag): number; @@ -142,13 +143,13 @@ export interface CdkDropListContainer { id: string; lockAxis: 'x' | 'y'; orientation: 'horizontal' | 'vertical'; - _canReturnItem(x: number, y: number): boolean; _getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropListContainer | null; + _isOverContainer(x: number, y: number): boolean; _sortItem(item: CdkDrag, pointerX: number, pointerY: number, delta: { x: number; y: number; }): void; - drop(item: CdkDrag, currentIndex: number, previousContainer?: CdkDropListContainer): void; + drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropListContainer, isPointerOverContainer: boolean): void; enter(item: CdkDrag, pointerX: number, pointerY: number): void; exit(item: CdkDrag): void; getItemIndex(item: CdkDrag): number;