diff --git a/src/cdk/drag-drop/directives/drag-preview.ts b/src/cdk/drag-drop/directives/drag-preview.ts index bd7674e0668a..ec925b6f4990 100644 --- a/src/cdk/drag-drop/directives/drag-preview.ts +++ b/src/cdk/drag-drop/directives/drag-preview.ts @@ -43,6 +43,9 @@ export class CdkDragPreview implements OnDestroy { /** Whether the preview should preserve the same size as the item that is being dragged. */ @Input({transform: booleanAttribute}) matchSize: boolean = false; + /** Whether the preview should snap the starting position centered under the cursor. */ + @Input({transform: booleanAttribute}) snapToCursor: boolean = false; + constructor(...args: unknown[]); constructor() { diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index b30c42a2fe5e..232340c9f5f4 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -437,6 +437,7 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { template: this._previewTemplate.templateRef, context: this._previewTemplate.data, matchSize: this._previewTemplate.matchSize, + snapToCursor: this._previewTemplate.snapToCursor, viewContainer: this._viewContainerRef, } : null; diff --git a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts index 20393fd6788a..a834b21a30da 100644 --- a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts +++ b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts @@ -823,6 +823,92 @@ export function defineCommonDropListTests(config: { expect(anchor!.parentNode).toBeFalsy(); })); + it('should display preview on mousedown for config.dragStartThreshold = 0', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZoneWithCustomPreview, { + providers: [ + { + provide: CDK_DRAG_CONFIG, + useValue: { + dragStartThreshold: 0, + }, + }, + ], + }); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + const itemRect = item.getBoundingClientRect(); + flush(); + dispatchMouseEvent(item, 'mousedown', undefined, undefined); + fixture.detectChanges(); + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + expect(preview).not.toBeNull(); + const previewRect = preview.getBoundingClientRect(); + expect(previewRect).not.toBeNull(); + })); + + it('should snap preview to cursor when snapToCursor is set on preview template', fakeAsync(() => { + @Component({ + styles: ` + .list { + display: flex; + width: 100px; + flex-direction: row; + } + + .item { + display: flex; + flex-grow: 1; + flex-basis: 0; + min-height: 50px; + } + `, + template: ` +
+ @for (item of items; track item) { +
+ {{item}} + +
{{item}}
+
+
+ } +
+ `, + imports: [CdkDropList, CdkDrag, CdkDragPreview], + }) + class DraggableInHorizontalFlexDropZoneWithSnapToCursorPreview { + @ViewChild(CdkDropList) dropInstance: CdkDropList; + @ViewChildren(CdkDrag) dragItems: QueryList; + items = ['Zero', 'One', 'Two']; + } + + const fixture = createComponent(DraggableInHorizontalFlexDropZoneWithSnapToCursorPreview); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + const listRect = + fixture.componentInstance.dropInstance.element.nativeElement.getBoundingClientRect(); + + startDraggingViaMouse(fixture, item); + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + + startDraggingViaMouse(fixture, item, listRect.right + 50, listRect.bottom + 50); + flush(); + dispatchMouseEvent(document, 'mousemove', listRect.right + 50, listRect.bottom + 50); + fixture.detectChanges(); + + const previewRect = preview.getBoundingClientRect(); + + // centered on the cursor + expect(Math.floor(previewRect.bottom - previewRect.height / 2)).toBe( + Math.floor(listRect.bottom + 50), + ); + expect(Math.floor(previewRect.right - previewRect.width / 2)).toBe( + Math.floor(listRect.right + 50), + ); + })); + it('should create a preview element while the item is dragged', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index 7296c3f59440..3c49683c8b33 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -677,7 +677,7 @@ export class DragRef { }; /** Handler that is invoked when the user moves their pointer after they've initiated a drag. */ - private _pointerMove = (event: MouseEvent | TouchEvent) => { + private _pointerMove = (event: MouseEvent | TouchEvent, mouseDown: boolean = false) => { const pointerPosition = this._getPointerPositionOnPage(event); if (!this._hasStartedDragging()) { @@ -711,8 +711,9 @@ export class DragRef { this._ngZone.run(() => this._startDragSequence(event)); } } - - return; + if (!mouseDown) { + return; + } } // We prevent the default action down here so that we know that dragging has started. This is @@ -977,6 +978,16 @@ export class DragRef { this._pointerPositionAtLastDirectionChange = {x: pointerPosition.x, y: pointerPosition.y}; this._dragStartTime = Date.now(); this._dragDropRegistry.startDragging(this, event); + + // when pixel threshold = 0 and dragStartDelay = 0 and a preview container/position exists we immediately drag + if ( + (event.type == 'mousedown' || event.type == 'touchstart') && + previewTemplate && + this._config.dragStartThreshold === 0 && + this._getDragStartDelay(event) === 0 + ) { + this._pointerMove(event, true); + } } /** Cleans up the DOM artifacts that were added to facilitate the element being dragged. */ @@ -1086,6 +1097,9 @@ export class DragRef { if (this.constrainPosition) { this._applyPreviewTransform(x, y); + } else if (this._previewTemplate?.snapToCursor) { + const previewRect = this._getPreviewRect(); + this._applyPreviewTransform(rawX - previewRect.width / 2, rawY - previewRect.height / 2); } else { this._applyPreviewTransform( x - this._pickupPositionInElement.x, diff --git a/src/cdk/drag-drop/preview-ref.ts b/src/cdk/drag-drop/preview-ref.ts index 7ffab115a7fb..8e65f46132de 100644 --- a/src/cdk/drag-drop/preview-ref.ts +++ b/src/cdk/drag-drop/preview-ref.ts @@ -21,6 +21,7 @@ import {getTransformTransitionDurationInMs} from './dom/transition-duration'; /** Template that can be used to create a drag preview element. */ export interface DragPreviewTemplate { matchSize?: boolean; + snapToCursor?: boolean; template: TemplateRef | null; viewContainer: ViewContainerRef; context: T;