Skip to content
Closed
112 changes: 62 additions & 50 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ViewChild,
ViewChildren,
ViewEncapsulation,
ValueProvider,
ChangeDetectionStrategy,
} from '@angular/core';
import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing';
Expand All @@ -28,30 +29,34 @@ import {CdkDropList} from './drop-list';
import {CdkDragHandle} from './drag-handle';
import {CdkDropListGroup} from './drop-list-group';
import {extendStyles} from '../drag-styling';
import {DragRefConfig} from '../drag-ref';
import {DragRefConfig, CdkDropStrategy} from '../drag-ref';

const ITEM_HEIGHT = 25;
const ITEM_WIDTH = 75;

describe('CdkDrag', () => {
function createComponent<T>(componentType: Type<T>, providers: Provider[] = [], dragDistance = 0):
ComponentFixture<T> {
TestBed.configureTestingModule({
imports: [DragDropModule],
declarations: [componentType, PassthroughComponent],
providers: [
{

if (providers.every((provider: any) =>
provider.provide && provider.provide !== CDK_DRAG_CONFIG)) {
providers.push({
provide: CDK_DRAG_CONFIG,
useValue: {
// We default the `dragDistance` to zero, because the majority of the tests
// don't care about it and drags are a lot easier to simulate when we don't
// have to deal with thresholds.
dragStartThreshold: dragDistance,
pointerDirectionChangeThreshold: 5
} as DragRefConfig
},
...providers
],
} as DragRefConfig});

}
TestBed.configureTestingModule({
imports: [DragDropModule],
declarations: [componentType, PassthroughComponent],
providers: [
...providers
],
}).compileComponents();

return TestBed.createComponent<T>(componentType);
Expand Down Expand Up @@ -1987,46 +1992,6 @@ describe('CdkDrag', () => {
expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled();
}));

it('should be able to move the element over a new container and return it to the initial ' +
'one, even if it no longer matches the enterPredicate', fakeAsync(() => {
const fixture = createComponent(ConnectedDropZones);
fixture.detectChanges();

const groups = fixture.componentInstance.groupedDragItems;
const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement);
const item = groups[0][1];
const initialRect = item.element.nativeElement.getBoundingClientRect();
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();

fixture.componentInstance.dropInstances.first.enterPredicate = () => false;
fixture.detectChanges();

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 + 1, targetRect.top + 1);
fixture.detectChanges();

expect(dropZones[1].contains(placeholder))
.toBe(true, 'Expected placeholder to be inside second container.');

dispatchMouseEvent(document, 'mousemove', initialRect.left + 1, initialRect.top + 1);
fixture.detectChanges();

expect(dropZones[0].contains(placeholder))
.toBe(true, 'Expected placeholder to be back inside first container.');

dispatchMouseEvent(document, 'mouseup');
fixture.detectChanges();

expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled();
}));

it('should transfer the DOM element from one drop zone to another', fakeAsync(() => {
const fixture = createComponent(ConnectedDropZones);
fixture.detectChanges();
Expand Down Expand Up @@ -2376,6 +2341,53 @@ describe('CdkDrag', () => {
expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled();
}));

it('should be able to move the element over a new container and return it when the dropping' +
' item is outside the drop container', fakeAsync(() => {

const fixture = createComponent(ConnectedDropZones,
[{
provide: CDK_DRAG_CONFIG,
useValue: {
dragStartThreshold: 0,
pointerDirectionChangeThreshold: 5,
dropStrategy: CdkDropStrategy.ExactLocation
} as DragRefConfig
} as ValueProvider]);
fixture.detectChanges();

const groups = fixture.componentInstance.groupedDragItems;
const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement);
const item = groups[0][1];
const targetRect = groups[1][2].element.nativeElement.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 + 1, targetRect.top + 1);

fixture.detectChanges();

expect(dropZones[1].contains(placeholder))
.toBe(true, 'Expected placeholder to be inside second container.');

dispatchMouseEvent(document, 'mousemove', targetRect.left + -5, targetRect.top - 5);
fixture.detectChanges();

expect(dropZones[0].contains(placeholder))
.toBe(true, 'Expected placeholder to be back inside first container.');

dispatchMouseEvent(document, 'mouseup');
fixture.detectChanges();

expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled();
}));


it('should not add child drop lists to the same group as their parents', fakeAsync(() => {
const fixture = createComponent(NestedDropListGroups);
const component = fixture.componentInstance;
Expand Down
5 changes: 3 additions & 2 deletions src/cdk/drag-drop/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {CdkDragPlaceholder} from './drag-placeholder';
import {CdkDragPreview} from './drag-preview';
import {CDK_DROP_LIST} from '../drop-list-container';
import {CDK_DRAG_PARENT} from '../drag-parent';
import {DragRef, DragRefConfig} from '../drag-ref';
import {DragRef, DragRefConfig, CdkDropStrategy} from '../drag-ref';
import {DropListRef} from '../drop-list-ref';
import {CdkDropListInternal as CdkDropList} from './drop-list';

Expand All @@ -56,7 +56,8 @@ export const CDK_DRAG_CONFIG = new InjectionToken<DragRefConfig>('CDK_DRAG_CONFI

/** @docs-private */
export function CDK_DRAG_CONFIG_FACTORY(): DragRefConfig {
return {dragStartThreshold: 5, pointerDirectionChangeThreshold: 5};
return {dragStartThreshold: 5, pointerDirectionChangeThreshold: 5,
dropStrategy: CdkDropStrategy.LastKnownContainer};
}

/** Element that can be moved inside a CdkDropList container. */
Expand Down
30 changes: 26 additions & 4 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ export interface DragRefConfig {
* considers them to have changed the drag direction.
*/
pointerDirectionChangeThreshold: number;

/**
* The strategy to take when dropping the item (in non drop zone area)
*/
dropStrategy: CdkDropStrategy;
}

/**
* Enum to decide what to do when the user drop the item
* LastKnownContainer - Drop the item on the Last container the item was dragged hover,
* no matter where the item is dropped.
* ExactLocation - Tries to drop the item in the current location,
* if the current location
* is not inside a valid drop zoom
* the item will return to the initial container.
*/
export enum CdkDropStrategy {
LastKnownContainer,
ExactLocation

}

/** Options that can be used to bind a passive event listener. */
Expand Down Expand Up @@ -664,9 +684,11 @@ export class DragRef<T = any> {
// 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._isOverContainer(x, y)) {
newContainer = this._initialContainer;
}
(this._initialContainer._isOverContainer(x, y) ||
(!this.dropContainer!._isOverContainer(x, y) &&
this._config.dropStrategy === CdkDropStrategy.ExactLocation))) {
newContainer = this._initialContainer;
}

if (newContainer) {
this._ngZone.run(() => {
Expand All @@ -676,7 +698,7 @@ export class DragRef<T = any> {
// Notify the new container that the item has entered.
this.entered.next({item: this, container: newContainer!});
this.dropContainer = newContainer!;
this.dropContainer.enter(this, x, y);
this.dropContainer!.enter(this, x, y);
});
}

Expand Down
8 changes: 7 additions & 1 deletion src/dev-app/drag-drop/drag-drop-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@
import {Component, ViewEncapsulation} from '@angular/core';
import {MatIconRegistry} from '@angular/material/icon';
import {DomSanitizer} from '@angular/platform-browser';
import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag-drop';
import {CDK_DRAG_CONFIG, CdkDragDrop, moveItemInArray,
transferArrayItem, CdkDragConfig} from '@angular/cdk/drag-drop';

@Component({
moduleId: module.id,
selector: 'drag-drop-demo',
templateUrl: 'drag-drop-demo.html',
styleUrls: ['drag-drop-demo.css'],
encapsulation: ViewEncapsulation.None,
providers: [{
provide: CDK_DRAG_CONFIG,
useValue: { dragStartThreshold: 5, pointerDirectionChangeThreshold: 5,
} as CdkDragConfig
}]
})
export class DragAndDropDemo {
axisLock: 'x' | 'y';
Expand Down