Skip to content

Commit 0939700

Browse files
committed
feat(drag-drop): indicate in dropped event whether item was dropped outside of container
Adds an extra flag on the `CdkDragDrop` event that indicates whether the user's pointer was over the container when they dropped an item. Fixes #14136.
1 parent 141afcf commit 0939700

File tree

6 files changed

+103
-38
lines changed

6 files changed

+103
-38
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export interface CdkDragDrop<T, O = T> {
5353
container: CdkDropListContainer<T>;
5454
/** Container from which the item was picked up. Can be the same as the `container`. */
5555
previousContainer: CdkDropListContainer<O>;
56+
/** Whether the user's pointer was over the container when the item was dropped. */
57+
isPointerOverContainer: boolean;
5658
}
5759

5860
/** Event emitted as the user is dragging a draggable item. */

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

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -765,13 +765,55 @@ describe('CdkDrag', () => {
765765
currentIndex: 2,
766766
item: firstItem,
767767
container: fixture.componentInstance.dropInstance,
768-
previousContainer: fixture.componentInstance.dropInstance
768+
previousContainer: fixture.componentInstance.dropInstance,
769+
isPointerOverContainer: true
769770
});
770771

771772
expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
772773
.toEqual(['One', 'Two', 'Zero', 'Three']);
773774
}));
774775

776+
it('should expose whether an item was dropped over a container', fakeAsync(() => {
777+
const fixture = createComponent(DraggableInDropZone);
778+
fixture.detectChanges();
779+
const dragItems = fixture.componentInstance.dragItems;
780+
const firstItem = dragItems.first;
781+
const thirdItemRect = dragItems.toArray()[2].element.nativeElement.getBoundingClientRect();
782+
783+
dragElementViaMouse(fixture, firstItem.element.nativeElement,
784+
thirdItemRect.left + 1, thirdItemRect.top + 1);
785+
flush();
786+
fixture.detectChanges();
787+
788+
expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1);
789+
790+
const event: CdkDragDrop<any> =
791+
fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];
792+
793+
expect(event.isPointerOverContainer).toBe(true);
794+
}));
795+
796+
it('should expose whether an item was dropped outside of a container', fakeAsync(() => {
797+
const fixture = createComponent(DraggableInDropZone);
798+
fixture.detectChanges();
799+
const dragItems = fixture.componentInstance.dragItems;
800+
const firstItem = dragItems.first;
801+
const containerRect = fixture.componentInstance.dropInstance.element
802+
.nativeElement.getBoundingClientRect();
803+
804+
dragElementViaMouse(fixture, firstItem.element.nativeElement,
805+
containerRect.right + 10, containerRect.bottom + 10);
806+
flush();
807+
fixture.detectChanges();
808+
809+
expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1);
810+
811+
const event: CdkDragDrop<any> =
812+
fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];
813+
814+
expect(event.isPointerOverContainer).toBe(false);
815+
}));
816+
775817
it('should dispatch the `sorted` event as an item is being sorted', fakeAsync(() => {
776818
const fixture = createComponent(DraggableInDropZone);
777819
fixture.detectChanges();
@@ -830,7 +872,8 @@ describe('CdkDrag', () => {
830872
currentIndex: 0,
831873
item: firstItem,
832874
container: fixture.componentInstance.dropInstance,
833-
previousContainer: fixture.componentInstance.dropInstance
875+
previousContainer: fixture.componentInstance.dropInstance,
876+
isPointerOverContainer: false
834877
});
835878

836879
expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
@@ -887,7 +930,8 @@ describe('CdkDrag', () => {
887930
currentIndex: 2,
888931
item: firstItem,
889932
container: fixture.componentInstance.dropInstance,
890-
previousContainer: fixture.componentInstance.dropInstance
933+
previousContainer: fixture.componentInstance.dropInstance,
934+
isPointerOverContainer: true
891935
});
892936

893937
expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
@@ -926,7 +970,8 @@ describe('CdkDrag', () => {
926970
currentIndex: 2,
927971
item: firstItem,
928972
container: fixture.componentInstance.dropInstance,
929-
previousContainer: fixture.componentInstance.dropInstance
973+
previousContainer: fixture.componentInstance.dropInstance,
974+
isPointerOverContainer: true
930975
});
931976

932977
expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
@@ -961,7 +1006,8 @@ describe('CdkDrag', () => {
9611006
currentIndex: 0,
9621007
item: firstItem,
9631008
container: fixture.componentInstance.dropInstance,
964-
previousContainer: fixture.componentInstance.dropInstance
1009+
previousContainer: fixture.componentInstance.dropInstance,
1010+
isPointerOverContainer: false
9651011
});
9661012

9671013
expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
@@ -1797,7 +1843,8 @@ describe('CdkDrag', () => {
17971843
currentIndex: 3,
17981844
item,
17991845
container: fixture.componentInstance.dropInstances.toArray()[1],
1800-
previousContainer: fixture.componentInstance.dropInstances.first
1846+
previousContainer: fixture.componentInstance.dropInstances.first,
1847+
isPointerOverContainer: true
18011848
});
18021849
}));
18031850

@@ -1898,7 +1945,8 @@ describe('CdkDrag', () => {
18981945
currentIndex: 3,
18991946
item: groups[0][1],
19001947
container: dropInstances[1],
1901-
previousContainer: dropInstances[0]
1948+
previousContainer: dropInstances[0],
1949+
isPointerOverContainer: true
19021950
});
19031951
}));
19041952

@@ -1927,7 +1975,8 @@ describe('CdkDrag', () => {
19271975
currentIndex: 1,
19281976
item: groups[0][1],
19291977
container: dropInstances[0],
1930-
previousContainer: dropInstances[0]
1978+
previousContainer: dropInstances[0],
1979+
isPointerOverContainer: false
19311980
});
19321981
}));
19331982

@@ -1956,7 +2005,8 @@ describe('CdkDrag', () => {
19562005
currentIndex: 1,
19572006
item: groups[0][1],
19582007
container: dropInstances[0],
1959-
previousContainer: dropInstances[0]
2008+
previousContainer: dropInstances[0],
2009+
isPointerOverContainer: false
19602010
});
19612011
}));
19622012

@@ -2078,7 +2128,8 @@ describe('CdkDrag', () => {
20782128
currentIndex: 3,
20792129
item: groups[0][1],
20802130
container: dropInstances[1],
2081-
previousContainer: dropInstances[0]
2131+
previousContainer: dropInstances[0],
2132+
isPointerOverContainer: true
20822133
});
20832134
}));
20842135

@@ -2103,7 +2154,8 @@ describe('CdkDrag', () => {
21032154
currentIndex: 3,
21042155
item: groups[0][1],
21052156
container: dropInstances[1],
2106-
previousContainer: dropInstances[0]
2157+
previousContainer: dropInstances[0],
2158+
isPointerOverContainer: true
21072159
});
21082160
}));
21092161

@@ -2133,7 +2185,8 @@ describe('CdkDrag', () => {
21332185
currentIndex: 3,
21342186
item: groups[0][1],
21352187
container: dropInstances[1],
2136-
previousContainer: dropInstances[0]
2188+
previousContainer: dropInstances[0],
2189+
isPointerOverContainer: true
21372190
});
21382191
}));
21392192

@@ -2167,7 +2220,8 @@ describe('CdkDrag', () => {
21672220
currentIndex: 0,
21682221
item,
21692222
container: fixture.componentInstance.dropInstances.toArray()[1],
2170-
previousContainer: fixture.componentInstance.dropInstances.first
2223+
previousContainer: fixture.componentInstance.dropInstances.first,
2224+
isPointerOverContainer: true
21712225
});
21722226

21732227
expect(dropContainers[0].contains(item.element.nativeElement)).toBe(true,
@@ -2656,7 +2710,7 @@ function dragElementViaMouse(fixture: ComponentFixture<any>,
26562710
dispatchMouseEvent(document, 'mousemove', x, y);
26572711
fixture.detectChanges();
26582712

2659-
dispatchMouseEvent(document, 'mouseup');
2713+
dispatchMouseEvent(document, 'mouseup', x, y);
26602714
fixture.detectChanges();
26612715
}
26622716

@@ -2695,7 +2749,7 @@ function dragElementViaTouch(fixture: ComponentFixture<any>,
26952749
dispatchTouchEvent(document, 'touchmove', x, y);
26962750
fixture.detectChanges();
26972751

2698-
dispatchTouchEvent(document, 'touchend');
2752+
dispatchTouchEvent(document, 'touchend', x, y);
26992753
fixture.detectChanges();
27002754
}
27012755

src/cdk/drag-drop/drag.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
515515
}
516516

517517
/** Handler that is invoked when the user lifts their pointer up, after initiating a drag. */
518-
private _pointerUp = () => {
518+
private _pointerUp = (event: MouseEvent | TouchEvent) => {
519519
if (!this._isDragging()) {
520520
return;
521521
}
@@ -539,13 +539,13 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
539539
}
540540

541541
this._animatePreviewToPlaceholder().then(() => {
542-
this._cleanupDragArtifacts();
542+
this._cleanupDragArtifacts(event);
543543
this._dragDropRegistry.stopDragging(this);
544544
});
545545
}
546546

547547
/** Cleans up the DOM artifacts that were added to facilitate the element being dragged. */
548-
private _cleanupDragArtifacts() {
548+
private _cleanupDragArtifacts(event: MouseEvent | TouchEvent) {
549549
// Restore the element's visibility and insert it at its old position in the DOM.
550550
// It's important that we maintain the position, because moving the element around in the DOM
551551
// can throw off `NgFor` which does smart diffing and re-creates elements only when necessary,
@@ -564,16 +564,19 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
564564
// Re-enter the NgZone since we bound `document` events on the outside.
565565
this._ngZone.run(() => {
566566
const currentIndex = this.dropContainer.getItemIndex(this);
567+
const {x, y} = this._getPointerPositionOnPage(event);
568+
const isPointerOverContainer = this.dropContainer._isOverContainer(x, y);
567569

568570
this.ended.emit({source: this});
569571
this.dropped.emit({
570572
item: this,
571573
currentIndex,
572574
previousIndex: this._initialContainer.getItemIndex(this),
573575
container: this.dropContainer,
574-
previousContainer: this._initialContainer
576+
previousContainer: this._initialContainer,
577+
isPointerOverContainer
575578
});
576-
this.dropContainer.drop(this, currentIndex, this._initialContainer);
579+
this.dropContainer.drop(this, currentIndex, this._initialContainer, isPointerOverContainer);
577580
this.dropContainer = this._initialContainer;
578581
});
579582
}
@@ -587,11 +590,11 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
587590
let newContainer = this.dropContainer._getSiblingContainerFromPosition(this, x, y);
588591

589592
// If we couldn't find a new container to move the item into, and the item has left it's
590-
// initial container, check whether the it's allowed to return into its original container.
591-
// This handles the case where two containers are connected one way and the user tries to
592-
// undo dragging an item into a new container.
593+
// initial container, check whether the it's over the initial container. This handles the
594+
// case where two containers are connected one way and the user tries to undo dragging an
595+
// item into a new container.
593596
if (!newContainer && this.dropContainer !== this._initialContainer &&
594-
this._initialContainer._canReturnItem(x, y)) {
597+
this._initialContainer._isOverContainer(x, y)) {
595598
newContainer = this._initialContainer;
596599
}
597600

src/cdk/drag-drop/drop-list-container.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@ export interface CdkDropListContainer<T = any> {
3737
* @param item Item being dropped into the container.
3838
* @param currentIndex Index at which the item should be inserted.
3939
* @param previousContainer Container from which the item got dragged in.
40+
* @param isPointerOverContainer Whether the user's pointer was over the
41+
* container when the item was dropped.
4042
*/
41-
drop(item: CdkDrag, currentIndex: number, previousContainer?: CdkDropListContainer): void;
43+
drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropListContainer,
44+
isPointerOverContainer: boolean): void;
4245

4346
/**
4447
* Emits an event to indicate that the user moved an item into the container.
@@ -63,7 +66,7 @@ export interface CdkDropListContainer<T = any> {
6366
_draggables: QueryList<CdkDrag>;
6467
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number):
6568
CdkDropListContainer | null;
66-
_canReturnItem(x: number, y: number): boolean;
69+
_isOverContainer(x: number, y: number): boolean;
6770
}
6871

6972
/**

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,16 +210,19 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
210210
* @param item Item being dropped into the container.
211211
* @param currentIndex Index at which the item should be inserted.
212212
* @param previousContainer Container from which the item got dragged in.
213+
* @param isPointerOverContainer Whether the user's pointer was over the
214+
* container when the item was dropped.
213215
*/
214-
drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropList): void {
216+
drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropList,
217+
isPointerOverContainer: boolean): void {
215218
this._reset();
216219
this.dropped.emit({
217220
item,
218221
currentIndex,
219222
previousIndex: previousContainer.getItemIndex(item),
220223
container: this,
221-
// TODO(crisbeto): reconsider whether to make this null if the containers are the same.
222-
previousContainer
224+
previousContainer,
225+
isPointerOverContainer
223226
});
224227
}
225228

@@ -386,12 +389,11 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
386389
}
387390

388391
/**
389-
* Checks whether an item that started in this container can be returned to it,
390-
* after it was moved out into another container.
391-
* @param x Position of the item along the X axis.
392-
* @param y Position of the item along the Y axis.
392+
* Checks whether the user's pointer is positioned over the container.
393+
* @param x Pointer position along the X axis.
394+
* @param y Pointer position along the Y axis.
393395
*/
394-
_canReturnItem(x: number, y: number): boolean {
396+
_isOverContainer(x: number, y: number): boolean {
395397
return isInsideClientRect(this._positionCache.self, x, y);
396398
}
397399

tools/public_api_guard/cdk/drag-drop.d.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface CdkDragConfig {
4141
export interface CdkDragDrop<T, O = T> {
4242
container: CdkDropListContainer<T>;
4343
currentIndex: number;
44+
isPointerOverContainer: boolean;
4445
item: CdkDrag;
4546
previousContainer: CdkDropListContainer<O>;
4647
previousIndex: number;
@@ -119,13 +120,13 @@ export declare class CdkDropList<T = any> implements OnInit, OnDestroy {
119120
orientation: 'horizontal' | 'vertical';
120121
sorted: EventEmitter<CdkDragSortEvent<T>>;
121122
constructor(element: ElementRef<HTMLElement>, _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup<CdkDropList<any>> | undefined);
122-
_canReturnItem(x: number, y: number): boolean;
123123
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropList | null;
124+
_isOverContainer(x: number, y: number): boolean;
124125
_sortItem(item: CdkDrag, pointerX: number, pointerY: number, pointerDelta: {
125126
x: number;
126127
y: number;
127128
}): void;
128-
drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropList): void;
129+
drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropList, isPointerOverContainer: boolean): void;
129130
enter(item: CdkDrag, pointerX: number, pointerY: number): void;
130131
exit(item: CdkDrag): void;
131132
getItemIndex(item: CdkDrag): number;
@@ -142,13 +143,13 @@ export interface CdkDropListContainer<T = any> {
142143
id: string;
143144
lockAxis: 'x' | 'y';
144145
orientation: 'horizontal' | 'vertical';
145-
_canReturnItem(x: number, y: number): boolean;
146146
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropListContainer | null;
147+
_isOverContainer(x: number, y: number): boolean;
147148
_sortItem(item: CdkDrag, pointerX: number, pointerY: number, delta: {
148149
x: number;
149150
y: number;
150151
}): void;
151-
drop(item: CdkDrag, currentIndex: number, previousContainer?: CdkDropListContainer): void;
152+
drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropListContainer, isPointerOverContainer: boolean): void;
152153
enter(item: CdkDrag, pointerX: number, pointerY: number): void;
153154
exit(item: CdkDrag): void;
154155
getItemIndex(item: CdkDrag): number;

0 commit comments

Comments
 (0)