diff --git a/src/cdk/drag-drop/drag.spec.ts b/src/cdk/drag-drop/drag.spec.ts index 4e2b4118741..a5b7b781b7b 100644 --- a/src/cdk/drag-drop/drag.spec.ts +++ b/src/cdk/drag-drop/drag.spec.ts @@ -18,6 +18,7 @@ import { dispatchEvent, dispatchMouseEvent, dispatchTouchEvent, + createTouchEvent, } from '@angular/cdk/testing'; import {Directionality} from '@angular/cdk/bidi'; import {CdkDrag, CDK_DRAG_CONFIG, CdkDragConfig} from './drag'; @@ -169,6 +170,25 @@ describe('CdkDrag', () => { expect(dragElement.style.transform).toBe('translateY(-50%) translate3d(150px, 300px, 0px)'); })); + it('should prevent the `mousedown` action for native draggable elements', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElement.draggable = true; + + const mousedownEvent = createMouseEvent('mousedown', 50, 50); + Object.defineProperty(mousedownEvent, 'target', {get: () => dragElement}); + spyOn(mousedownEvent, 'preventDefault').and.callThrough(); + dispatchEvent(dragElement, mousedownEvent); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mousemove', 50, 50); + fixture.detectChanges(); + + expect(mousedownEvent.preventDefault).toHaveBeenCalled(); + })); + }); describe('touch dragging', () => { @@ -244,6 +264,25 @@ describe('CdkDrag', () => { dispatchTouchEvent(document, 'touchend'); fixture.detectChanges(); })); + + it('should not prevent `touchstart` action for native draggable elements', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElement.draggable = true; + + const touchstartEvent = createTouchEvent('touchstart', 50, 50); + Object.defineProperty(touchstartEvent, 'target', {get: () => dragElement}); + spyOn(touchstartEvent, 'preventDefault').and.callThrough(); + dispatchEvent(dragElement, touchstartEvent); + fixture.detectChanges(); + + dispatchTouchEvent(document, 'touchmove'); + fixture.detectChanges(); + + expect(touchstartEvent.preventDefault).not.toHaveBeenCalled(); + })); }); it('should dispatch an event when the user has started dragging', fakeAsync(() => { diff --git a/src/cdk/drag-drop/drag.ts b/src/cdk/drag-drop/drag.ts index b079a0bff7c..441f7160a6b 100644 --- a/src/cdk/drag-drop/drag.ts +++ b/src/cdk/drag-drop/drag.ts @@ -82,6 +82,9 @@ export function CDK_DRAG_CONFIG_FACTORY(): CdkDragConfig { /** Options that can be used to bind a passive event listener. */ const passiveEventListenerOptions = normalizePassiveListenerOptions({passive: true}); +/** Options that can be used to bind an active event listener. */ +const activeEventListenerOptions = normalizePassiveListenerOptions({passive: false}); + /** Element that can be moved inside a CdkDropList container. */ @Directive({ selector: '[cdkDrag]', @@ -279,7 +282,7 @@ export class CdkDrag implements AfterViewInit, OnDestroy { .pipe(take(1)) .subscribe(() => { const rootElement = this._rootElement = this._getRootElement(); - rootElement.addEventListener('mousedown', this._pointerDown, passiveEventListenerOptions); + rootElement.addEventListener('mousedown', this._pointerDown, activeEventListenerOptions); rootElement.addEventListener('touchstart', this._pointerDown, passiveEventListenerOptions); this._handles.changes.pipe(startWith(null)).subscribe(() => toggleNativeDragInteractions(rootElement, this.getChildHandles().length > 0)); @@ -290,7 +293,7 @@ export class CdkDrag implements AfterViewInit, OnDestroy { // The directive might have been destroyed before the root element is initialized. if (this._rootElement) { this._rootElement.removeEventListener('mousedown', this._pointerDown, - passiveEventListenerOptions); + activeEventListenerOptions); this._rootElement.removeEventListener('touchstart', this._pointerDown, passiveEventListenerOptions); @@ -354,6 +357,16 @@ export class CdkDrag implements AfterViewInit, OnDestroy { // starting another sequence for a draggable parent somewhere up the DOM tree. event.stopPropagation(); + // If the event started from an element with the native HTML drag&drop, it'll interfere + // with our own dragging (e.g. `img` tags do it by default). Prevent the default action + // to stop it from happening. Note that preventing on `dragstart` also seems to work, but + // it's flaky and it fails if the user drags it away quickly. Also note that we only want + // to do this for `mousedown` since doing the same for `touchstart` will stop any `click` + // events from firing on touch devices. + if (event.target && (event.target as HTMLElement).draggable && event.type === 'mousedown') { + event.preventDefault(); + } + // Abort if the user is already dragging or is using a mouse button other than the primary one. if (this._isDragging() || (!this._isTouchEvent(event) && event.button !== 0)) { return;