Skip to content

Commit 58b8d30

Browse files
authored
fix(drag-drop): drop-list wrong enter position (#19116)
* fix(drag-drop): drop-list wrong enter position Parent rect should be cached again when the new item has been added. If item is entering from start (left/top) it should be inserted in the first position. When looking for item index from pointer position exclude right/bottom pixel: that pixel is external. * Corrected a bug when entering with no active draggables. * Corrected enter behavior on reversed container Added unit tests to cover fixed scenarios * Fixed view-engine broken test
1 parent d319e56 commit 58b8d30

File tree

5 files changed

+201
-19
lines changed

5 files changed

+201
-19
lines changed

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

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4013,9 +4013,11 @@ describe('CdkDrag', () => {
40134013
const groups = fixture.componentInstance.groupedDragItems.slice();
40144014
const element = groups[0][1].element.nativeElement;
40154015
const dropInstances = fixture.componentInstance.dropInstances.toArray();
4016-
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
4016+
const targetRect = groups[1][1].element.nativeElement.getBoundingClientRect();
40174017

4018-
dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1);
4018+
// Use coordinates of [1] item corresponding to [2] item
4019+
// after dragged item is removed from first container
4020+
dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top);
40194021
flush();
40204022
fixture.detectChanges();
40214023

@@ -4024,7 +4026,7 @@ describe('CdkDrag', () => {
40244026
expect(event).toBeTruthy();
40254027
expect(event).toEqual({
40264028
previousIndex: 1,
4027-
currentIndex: 3,
4029+
currentIndex: 2, // dragged item should replace the [2] item (see comment above)
40284030
item: groups[0][1],
40294031
container: dropInstances[1],
40304032
previousContainer: dropInstances[0],
@@ -4203,6 +4205,132 @@ describe('CdkDrag', () => {
42034205
expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled();
42044206
}));
42054207

4208+
it('should update drop zone after element has entered', fakeAsync(() => {
4209+
const fixture = createComponent(ConnectedDropZones);
4210+
4211+
// Make sure there's only one item in the first list.
4212+
fixture.componentInstance.todo = ['things'];
4213+
fixture.detectChanges();
4214+
4215+
const dropInstances = fixture.componentInstance.dropInstances.toArray();
4216+
const groups = fixture.componentInstance.groupedDragItems;
4217+
const dropZones = dropInstances.map(d => d.element.nativeElement);
4218+
const item = groups[0][0];
4219+
const initialTargetZoneRect = dropZones[1].getBoundingClientRect();
4220+
const targetElement = groups[1][groups[1].length - 1].element.nativeElement;
4221+
let targetRect = targetElement.getBoundingClientRect();
4222+
4223+
startDraggingViaMouse(fixture, item.element.nativeElement);
4224+
4225+
const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!;
4226+
4227+
expect(placeholder).toBeTruthy();
4228+
4229+
dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1);
4230+
fixture.detectChanges();
4231+
4232+
expect(targetElement.previousSibling === placeholder)
4233+
.toBe(true, 'Expected placeholder to be inside second container before last item.');
4234+
4235+
// Update target rect
4236+
targetRect = targetElement.getBoundingClientRect();
4237+
expect(initialTargetZoneRect.bottom <= targetRect.top)
4238+
.toBe(true, 'Expected target rect to be outside of initial target zone rect');
4239+
4240+
// Swap with target
4241+
dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.bottom - 1);
4242+
fixture.detectChanges();
4243+
4244+
// Drop and verify item drop positon and coontainer
4245+
dispatchMouseEvent(document, 'mouseup', targetRect.left + 1, targetRect.bottom - 1);
4246+
flush();
4247+
fixture.detectChanges();
4248+
4249+
const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];
4250+
4251+
expect(event).toBeTruthy();
4252+
expect(event).toEqual({
4253+
previousIndex: 0,
4254+
currentIndex: 3,
4255+
item: item,
4256+
container: dropInstances[1],
4257+
previousContainer: dropInstances[0],
4258+
isPointerOverContainer: true,
4259+
distance: {x: jasmine.any(Number), y: jasmine.any(Number)}
4260+
});
4261+
}));
4262+
4263+
it('should enter as first child if entering from top', fakeAsync(() => {
4264+
const fixture = createComponent(ConnectedDropZones);
4265+
4266+
// Make sure there's only one item in the first list.
4267+
fixture.componentInstance.todo = ['things'];
4268+
fixture.detectChanges();
4269+
4270+
const groups = fixture.componentInstance.groupedDragItems;
4271+
const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement);
4272+
const item = groups[0][0];
4273+
4274+
// Add some initial padding as the target drop zone
4275+
dropZones[1].style.paddingTop = '10px';
4276+
4277+
const targetRect = dropZones[1].getBoundingClientRect();
4278+
4279+
startDraggingViaMouse(fixture, item.element.nativeElement);
4280+
4281+
const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!;
4282+
4283+
expect(placeholder).toBeTruthy();
4284+
4285+
expect(dropZones[0].contains(placeholder))
4286+
.toBe(true, 'Expected placeholder to be inside the first container.');
4287+
4288+
dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top);
4289+
fixture.detectChanges();
4290+
4291+
expect(dropZones[1].firstElementChild === placeholder)
4292+
.toBe(true, 'Expected placeholder to be first child inside second container.');
4293+
4294+
dispatchMouseEvent(document, 'mouseup');
4295+
}));
4296+
4297+
it('should enter as last child if entering from top in reversed container', fakeAsync(() => {
4298+
const fixture = createComponent(ConnectedDropZones);
4299+
4300+
// Make sure there's only one item in the first list.
4301+
fixture.componentInstance.todo = ['things'];
4302+
fixture.detectChanges();
4303+
4304+
const groups = fixture.componentInstance.groupedDragItems;
4305+
const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement);
4306+
const item = groups[0][0];
4307+
4308+
// Add some initial padding as the target drop zone
4309+
const targetDropZoneStyle = dropZones[1].style;
4310+
targetDropZoneStyle.paddingTop = '10px';
4311+
targetDropZoneStyle.display = 'flex';
4312+
targetDropZoneStyle.flexDirection = 'column-reverse';
4313+
4314+
const targetRect = dropZones[1].getBoundingClientRect();
4315+
4316+
startDraggingViaMouse(fixture, item.element.nativeElement);
4317+
4318+
const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!;
4319+
4320+
expect(placeholder).toBeTruthy();
4321+
4322+
expect(dropZones[0].contains(placeholder))
4323+
.toBe(true, 'Expected placeholder to be inside the first container.');
4324+
4325+
dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top);
4326+
fixture.detectChanges();
4327+
4328+
expect(dropZones[1].lastChild === placeholder)
4329+
.toBe(true, 'Expected placeholder to be last child inside second container.');
4330+
4331+
dispatchMouseEvent(document, 'mouseup');
4332+
}));
4333+
42064334
it('should assign a default id on each drop zone', fakeAsync(() => {
42074335
const fixture = createComponent(ConnectedDropZones);
42084336
fixture.detectChanges();

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

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -302,16 +302,24 @@ export class DropListRef<T = any> {
302302
element.parentElement!.insertBefore(placeholder, element);
303303
activeDraggables.splice(newIndex, 0, item);
304304
} else {
305-
coerceElement(this.element).appendChild(placeholder);
306-
activeDraggables.push(item);
305+
const element = coerceElement(this.element);
306+
if (this._shouldEnterAsFirstChild(pointerX, pointerY)) {
307+
element.insertBefore(placeholder, activeDraggables[0].getRootElement());
308+
activeDraggables.unshift(item);
309+
} else {
310+
element.appendChild(placeholder);
311+
activeDraggables.push(item);
312+
}
307313
}
308314

309315
// The transform needs to be cleared so it doesn't throw off the measurements.
310316
placeholder.style.transform = '';
311317

312318
// Note that the positions were already cached when we called `start` above,
313-
// but we need to refresh them since the amount of items has changed.
319+
// but we need to refresh them since the amount of items has changed and also parent rects.
314320
this._cacheItemPositions();
321+
this._cacheParentPositions();
322+
315323
this.entered.next({item, container: this, currentIndex: this.getItemIndex(item)});
316324
}
317325

@@ -695,6 +703,31 @@ export class DropListRef<T = any> {
695703
return itemOffset;
696704
}
697705

706+
/**
707+
* Checks if pointer is entering in the first position
708+
* @param pointerX Position of the user's pointer along the X axis.
709+
* @param pointerY Position of the user's pointer along the Y axis.
710+
*/
711+
private _shouldEnterAsFirstChild(pointerX: number, pointerY: number) {
712+
if (!this._activeDraggables.length) {
713+
return false;
714+
}
715+
716+
const itemPositions = this._itemPositions;
717+
const isHorizontal = this._orientation === 'horizontal';
718+
719+
// `itemPositions` are sorted by position while `activeDraggables` are sorted by child index
720+
// check if container is using some sort of "reverse" ordering (eg: flex-direction: row-reverse)
721+
const reversed = itemPositions[0].drag !== this._activeDraggables[0];
722+
if (reversed) {
723+
const lastItemRect = itemPositions[itemPositions.length - 1].clientRect;
724+
return isHorizontal ? pointerX >= lastItemRect.right : pointerY >= lastItemRect.bottom;
725+
} else {
726+
const firstItemRect = itemPositions[0].clientRect;
727+
return isHorizontal ? pointerX <= firstItemRect.left : pointerY <= firstItemRect.top;
728+
}
729+
}
730+
698731
/**
699732
* Gets the index of an item in the drop container, based on the position of the user's pointer.
700733
* @param item Item that is being sorted.
@@ -726,8 +759,8 @@ export class DropListRef<T = any> {
726759
return isHorizontal ?
727760
// Round these down since most browsers report client rects with
728761
// sub-pixel precision, whereas the pointer coordinates are rounded to pixels.
729-
pointerX >= Math.floor(clientRect.left) && pointerX <= Math.floor(clientRect.right) :
730-
pointerY >= Math.floor(clientRect.top) && pointerY <= Math.floor(clientRect.bottom);
762+
pointerX >= Math.floor(clientRect.left) && pointerX < Math.floor(clientRect.right) :
763+
pointerY >= Math.floor(clientRect.top) && pointerY < Math.floor(clientRect.bottom);
731764
});
732765
}
733766

src/dev-app/drag-drop/drag-drop-demo.html

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,31 @@ <h2>Done</h2>
2929
</div>
3030
</div>
3131

32-
<div>
32+
<div cdkDropListGroup>
3333
<div class="demo-list demo-list-horizontal">
34-
<h2>Horizontal list</h2>
34+
<h2>Ages</h2>
35+
<div
36+
cdkDropList
37+
cdkDropListOrientation="horizontal"
38+
(cdkDropListDropped)="drop($event)"
39+
[cdkDropListLockAxis]="axisLock"
40+
[cdkDropListData]="ages">
41+
<div *ngFor="let item of ages" cdkDrag>
42+
{{item}}
43+
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
44+
</div>
45+
</div>
46+
</div>
47+
48+
<div class="demo-list demo-list-horizontal" style="text-align: right">
49+
<h2>Preferred Ages</h2>
3550
<div
3651
cdkDropList
3752
cdkDropListOrientation="horizontal"
3853
(cdkDropListDropped)="drop($event)"
3954
[cdkDropListLockAxis]="axisLock"
40-
[cdkDropListData]="horizontalData">
41-
<div *ngFor="let item of horizontalData" cdkDrag>
55+
[cdkDropListData]="preferredAges">
56+
<div *ngFor="let item of preferredAges" cdkDrag>
4257
{{item}}
4358
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
4459
</div>
@@ -55,7 +70,8 @@ <h2>Free dragging</h2>
5570
<h2>Data</h2>
5671
<pre>{{todo.join(', ')}}</pre>
5772
<pre>{{done.join(', ')}}</pre>
58-
<pre>{{horizontalData.join(', ')}}</pre>
73+
<pre>{{ages.join(', ')}}</pre>
74+
<pre>{{preferredAges.join(', ')}}</pre>
5975
</div>
6076

6177
<div>

src/dev-app/drag-drop/drag-drop-demo.scss

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
display: block;
2525

2626
.demo-list-horizontal & {
27-
display: flex;
27+
padding: 0 24px;
28+
display: inline-flex;
2829
flex-direction: row;
2930
}
3031
}
@@ -49,8 +50,9 @@
4950
.demo-list-horizontal & {
5051
border: none;
5152
border-right: solid 1px #ccc;
52-
flex-grow: 1;
53-
flex-basis: 0;
53+
flex: 1 1;
54+
white-space: nowrap;
55+
background-color: #fff;
5456

5557
[dir='rtl'] & {
5658
border-right: none;

src/dev-app/drag-drop/drag-drop-demo.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,15 @@ export class DragAndDropDemo {
3535
'Check reddit'
3636
];
3737

38-
horizontalData = [
38+
ages = [
39+
'Stone age',
3940
'Bronze age',
4041
'Iron age',
4142
'Middle ages',
42-
'Early modern period',
43-
'Long nineteenth century'
43+
];
44+
preferredAges = [
45+
'Modern period',
46+
'Renaissance'
4447
];
4548

4649
constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {

0 commit comments

Comments
 (0)