diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 01de1d6e92a1..862cf7f8bcdf 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -9,6 +9,7 @@ import { ViewChild, ViewChildren, ViewEncapsulation, + ValueProvider, ChangeDetectionStrategy, } from '@angular/core'; import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing'; @@ -28,7 +29,7 @@ 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; @@ -36,11 +37,10 @@ const ITEM_WIDTH = 75; describe('CdkDrag', () => { function createComponent(componentType: Type, providers: Provider[] = [], dragDistance = 0): ComponentFixture { - 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 @@ -48,10 +48,15 @@ describe('CdkDrag', () => { // 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(componentType); @@ -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(); @@ -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; diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index 1db945ca0656..315bf8536eab 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -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'; @@ -56,7 +56,8 @@ export const CDK_DRAG_CONFIG = new InjectionToken('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. */ diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index fd7e97a41af0..4c6efaf7a246 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -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. */ @@ -664,9 +684,11 @@ export class DragRef { // 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(() => { @@ -676,7 +698,7 @@ export class DragRef { // 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); }); } diff --git a/src/dev-app/drag-drop/drag-drop-demo.ts b/src/dev-app/drag-drop/drag-drop-demo.ts index c8683432ba8b..0ef62ae69b97 100644 --- a/src/dev-app/drag-drop/drag-drop-demo.ts +++ b/src/dev-app/drag-drop/drag-drop-demo.ts @@ -9,7 +9,8 @@ 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, @@ -17,6 +18,11 @@ import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag 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';