fix(drag-drop): avoid interference from native drag&drop (#14054)

Currently if the user starts dragging from an element that has the native drag&drop (e.g. `img` tags have it by default), the CDK dragging won't work correctly. These changes add a `preventDefault` to work around it.
This commit is contained in:
Kristiyan Kostadinov
2018-11-10 00:15:51 +00:00
committed by vivian-hu
parent 9b05c3cfba
commit 26d63ee10b
2 changed files with 54 additions and 2 deletions
+39
View File
@@ -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(() => {
+15 -2
View File
@@ -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<T = any> 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<T = any> 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<T = any> 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;