Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancel drag after hovering a drop zone #13879

Closed
wants to merge 12 commits into from
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