diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 942e9599fb35..a222d9e30c49 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -3916,9 +3916,11 @@ describe('CdkDrag', () => { 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 targetRect = groups[1][1].element.nativeElement.getBoundingClientRect(); - dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1); + // Use coordinates of [1] item corresponding to [2] item + // after dragged item is removed from first container + dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top); flush(); fixture.detectChanges(); @@ -3927,7 +3929,7 @@ describe('CdkDrag', () => { expect(event).toBeTruthy(); expect(event).toEqual({ previousIndex: 1, - currentIndex: 3, + currentIndex: 2, // dragged item should replace the [2] item (see comment above) item: groups[0][1], container: dropInstances[1], previousContainer: dropInstances[0], @@ -4106,6 +4108,132 @@ describe('CdkDrag', () => { expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); })); + it('should update drop zone after element has entered', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + + // Make sure there's only one item in the first list. + fixture.componentInstance.todo = ['things']; + fixture.detectChanges(); + + const dropInstances = fixture.componentInstance.dropInstances.toArray(); + const groups = fixture.componentInstance.groupedDragItems; + const dropZones = dropInstances.map(d => d.element.nativeElement); + const item = groups[0][0]; + const initialTargetZoneRect = dropZones[1].getBoundingClientRect(); + const targetElement = groups[1][groups[1].length - 1].element.nativeElement; + let targetRect = targetElement.getBoundingClientRect(); + + startDraggingViaMouse(fixture, item.element.nativeElement); + + const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; + + expect(placeholder).toBeTruthy(); + + dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); + fixture.detectChanges(); + + expect(targetElement.previousSibling === placeholder) + .toBe(true, 'Expected placeholder to be inside second container before last item.'); + + // Update target rect + targetRect = targetElement.getBoundingClientRect(); + expect(initialTargetZoneRect.bottom <= targetRect.top) + .toBe(true, 'Expected target rect to be outside of initial target zone rect'); + + // Swap with target + dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.bottom - 1); + fixture.detectChanges(); + + // Drop and verify item drop positon and coontainer + dispatchMouseEvent(document, 'mouseup', targetRect.left + 1, targetRect.bottom - 1); + flush(); + fixture.detectChanges(); + + const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + + expect(event).toBeTruthy(); + expect(event).toEqual({ + previousIndex: 0, + currentIndex: 3, + item: item, + container: dropInstances[1], + previousContainer: dropInstances[0], + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} + }); + })); + + it('should enter as first child if entering from top', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + + // Make sure there's only one item in the first list. + fixture.componentInstance.todo = ['things']; + fixture.detectChanges(); + + const groups = fixture.componentInstance.groupedDragItems; + const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); + const item = groups[0][0]; + + // Add some initial padding as the target drop zone + dropZones[1].style.paddingTop = '10px'; + + const targetRect = dropZones[1].getBoundingClientRect(); + + startDraggingViaMouse(fixture, item.element.nativeElement); + + const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; + + expect(placeholder).toBeTruthy(); + + expect(dropZones[0].contains(placeholder)) + .toBe(true, 'Expected placeholder to be inside the first container.'); + + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top); + fixture.detectChanges(); + + expect(dropZones[1].firstElementChild === placeholder) + .toBe(true, 'Expected placeholder to be first child inside second container.'); + + dispatchMouseEvent(document, 'mouseup'); + })); + + it('should enter as last child if entering from top in reversed container', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + + // Make sure there's only one item in the first list. + fixture.componentInstance.todo = ['things']; + fixture.detectChanges(); + + const groups = fixture.componentInstance.groupedDragItems; + const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); + const item = groups[0][0]; + + // Add some initial padding as the target drop zone + const targetDropZoneStyle = dropZones[1].style; + targetDropZoneStyle.paddingTop = '10px'; + targetDropZoneStyle.display = 'flex'; + targetDropZoneStyle.flexDirection = 'column-reverse'; + + const targetRect = dropZones[1].getBoundingClientRect(); + + startDraggingViaMouse(fixture, item.element.nativeElement); + + const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; + + expect(placeholder).toBeTruthy(); + + expect(dropZones[0].contains(placeholder)) + .toBe(true, 'Expected placeholder to be inside the first container.'); + + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top); + fixture.detectChanges(); + + expect(dropZones[1].lastChild === placeholder) + .toBe(true, 'Expected placeholder to be last child inside second container.'); + + dispatchMouseEvent(document, 'mouseup'); + })); + it('should assign a default id on each drop zone', fakeAsync(() => { const fixture = createComponent(ConnectedDropZones); fixture.detectChanges(); diff --git a/src/cdk/drag-drop/drop-list-ref.ts b/src/cdk/drag-drop/drop-list-ref.ts index b0722d34a9ba..1bd2bd67aed6 100644 --- a/src/cdk/drag-drop/drop-list-ref.ts +++ b/src/cdk/drag-drop/drop-list-ref.ts @@ -309,16 +309,24 @@ export class DropListRef { element.parentElement!.insertBefore(placeholder, element); activeDraggables.splice(newIndex, 0, item); } else { - coerceElement(this.element).appendChild(placeholder); - activeDraggables.push(item); + const element = coerceElement(this.element); + if (this._shouldEnterAsFirstChild(pointerX, pointerY)) { + element.insertBefore(placeholder, activeDraggables[0].getRootElement()); + activeDraggables.unshift(item); + } else { + element.appendChild(placeholder); + activeDraggables.push(item); + } } // The transform needs to be cleared so it doesn't throw off the measurements. placeholder.style.transform = ''; // Note that the positions were already cached when we called `start` above, - // but we need to refresh them since the amount of items has changed. + // but we need to refresh them since the amount of items has changed and also parent rects. this._cacheItemPositions(); + this._cacheParentPositions(); + this.entered.next({item, container: this, currentIndex: this.getItemIndex(item)}); } @@ -695,6 +703,31 @@ export class DropListRef { return itemOffset; } + /** + * Checks if pointer is entering in the first position + * @param pointerX Position of the user's pointer along the X axis. + * @param pointerY Position of the user's pointer along the Y axis. + */ + private _shouldEnterAsFirstChild(pointerX: number, pointerY: number) { + if (!this._activeDraggables.length) { + return false; + } + + const itemPositions = this._itemPositions; + const isHorizontal = this._orientation === 'horizontal'; + + // `itemPositions` are sorted by position while `activeDraggables` are sorted by child index + // check if container is using some sort of "reverse" ordering (eg: flex-direction: row-reverse) + const reversed = itemPositions[0].drag !== this._activeDraggables[0]; + if (reversed) { + const lastItemRect = itemPositions[itemPositions.length - 1].clientRect; + return isHorizontal ? pointerX >= lastItemRect.right : pointerY >= lastItemRect.bottom; + } else { + const firstItemRect = itemPositions[0].clientRect; + return isHorizontal ? pointerX <= firstItemRect.left : pointerY <= firstItemRect.top; + } + } + /** * Gets the index of an item in the drop container, based on the position of the user's pointer. * @param item Item that is being sorted. @@ -726,8 +759,8 @@ export class DropListRef { return isHorizontal ? // Round these down since most browsers report client rects with // sub-pixel precision, whereas the pointer coordinates are rounded to pixels. - pointerX >= Math.floor(clientRect.left) && pointerX <= Math.floor(clientRect.right) : - pointerY >= Math.floor(clientRect.top) && pointerY <= Math.floor(clientRect.bottom); + pointerX >= Math.floor(clientRect.left) && pointerX < Math.floor(clientRect.right) : + pointerY >= Math.floor(clientRect.top) && pointerY < Math.floor(clientRect.bottom); }); } diff --git a/src/dev-app/drag-drop/drag-drop-demo.html b/src/dev-app/drag-drop/drag-drop-demo.html index 2e7ca73813bd..8a599a0d37ec 100644 --- a/src/dev-app/drag-drop/drag-drop-demo.html +++ b/src/dev-app/drag-drop/drag-drop-demo.html @@ -29,16 +29,31 @@

Done

-
+
-

Horizontal list

+

Ages

+
+
+ {{item}} + +
+
+
+ +
+

Preferred Ages

-
+ [cdkDropListData]="preferredAges"> +
{{item}}
@@ -55,7 +70,8 @@

Free dragging

Data

{{todo.join(', ')}}
{{done.join(', ')}}
-
{{horizontalData.join(', ')}}
+
{{ages.join(', ')}}
+
{{preferredAges.join(', ')}}
diff --git a/src/dev-app/drag-drop/drag-drop-demo.scss b/src/dev-app/drag-drop/drag-drop-demo.scss index bacf64bf8bf4..ce97633fa757 100644 --- a/src/dev-app/drag-drop/drag-drop-demo.scss +++ b/src/dev-app/drag-drop/drag-drop-demo.scss @@ -24,7 +24,8 @@ display: block; .demo-list-horizontal & { - display: flex; + padding: 0 24px; + display: inline-flex; flex-direction: row; } } @@ -49,8 +50,9 @@ .demo-list-horizontal & { border: none; border-right: solid 1px #ccc; - flex-grow: 1; - flex-basis: 0; + flex: 1 1; + white-space: nowrap; + background-color: #fff; [dir='rtl'] & { border-right: none; diff --git a/src/dev-app/drag-drop/drag-drop-demo.ts b/src/dev-app/drag-drop/drag-drop-demo.ts index d62c35b6b3dd..a3b7eed54a2a 100644 --- a/src/dev-app/drag-drop/drag-drop-demo.ts +++ b/src/dev-app/drag-drop/drag-drop-demo.ts @@ -35,12 +35,15 @@ export class DragAndDropDemo { 'Check reddit' ]; - horizontalData = [ + ages = [ + 'Stone age', 'Bronze age', 'Iron age', 'Middle ages', - 'Early modern period', - 'Long nineteenth century' + ]; + preferredAges = [ + 'Modern period', + 'Renaissance' ]; constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {