From dc906158dcdead59adc94264aab53d4e762f5108 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 7 Jul 2021 07:21:49 +0200 Subject: [PATCH] fix(cdk/drag-drop): don't start dragging on fake screen reader events Fixes that the CDK drag&drop could be triggered by fake `mousedown` or `touchstart` events dispatched by a screen reader. --- src/cdk/drag-drop/BUILD.bazel | 1 + src/cdk/drag-drop/directives/drag.spec.ts | 55 +++++++++++++++++++++++ src/cdk/drag-drop/drag-ref.ts | 8 +++- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/cdk/drag-drop/BUILD.bazel b/src/cdk/drag-drop/BUILD.bazel index 8c5c989007bb..bbd4d7bbefd5 100644 --- a/src/cdk/drag-drop/BUILD.bazel +++ b/src/cdk/drag-drop/BUILD.bazel @@ -17,6 +17,7 @@ ng_module( module_name = "@angular/cdk/drag-drop", deps = [ "//src:dev_mode_types", + "//src/cdk/a11y", "//src/cdk/bidi", "//src/cdk/coercion", "//src/cdk/platform", diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index c7643e4e135b..f452f7e29963 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -253,6 +253,33 @@ describe('CdkDrag', () => { expect(mousedownEvent.preventDefault).toHaveBeenCalled(); })); + it('should not start dragging an element with a fake mousedown event', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const event = createMouseEvent('mousedown', 0, 0); + + Object.defineProperties(event, { + offsetX: {get: () => 0}, + offsetY: {get: () => 0} + }); + + expect(dragElement.style.transform).toBeFalsy(); + + dispatchEvent(dragElement, event); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mousemove', 20, 100); + fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + + expect(dragElement.style.transform).toBeFalsy(); + })); + }); describe('touch dragging', () => { @@ -350,6 +377,34 @@ describe('CdkDrag', () => { expect(touchstartEvent.preventDefault).not.toHaveBeenCalled(); })); + + it('should not start dragging an element with a fake touchstart event', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const event = createTouchEvent('touchstart', 50, 50) as TouchEvent; + + Object.defineProperties(event.touches[0], { + identifier: {get: () => -1}, + radiusX: {get: () => null}, + radiusY: {get: () => null} + }); + + expect(dragElement.style.transform).toBeFalsy(); + + dispatchEvent(dragElement, event); + fixture.detectChanges(); + + dispatchTouchEvent(document, 'touchmove', 20, 100); + fixture.detectChanges(); + dispatchTouchEvent(document, 'touchmove', 50, 100); + fixture.detectChanges(); + + dispatchTouchEvent(document, 'touchend'); + fixture.detectChanges(); + + expect(dragElement.style.transform).toBeFalsy(); + })); }); it('should dispatch an event when the user has started dragging', fakeAsync(() => { diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index db00e8fea55d..da8a5481be85 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -11,6 +11,10 @@ import {ViewportRuler} from '@angular/cdk/scrolling'; import {Direction} from '@angular/cdk/bidi'; import {normalizePassiveListenerOptions, _getShadowRoot} from '@angular/cdk/platform'; import {coerceBooleanProperty, coerceElement} from '@angular/cdk/coercion'; +import { + isFakeMousedownFromScreenReader, + isFakeTouchstartFromScreenReader, +} from '@angular/cdk/a11y'; import {Subscription, Subject, Observable} from 'rxjs'; import {DropListRefInternal as DropListRef} from './drop-list-ref'; import {DragDropRegistry} from './drag-drop-registry'; @@ -854,6 +858,8 @@ export class DragRef { const target = getEventTarget(event); const isSyntheticEvent = !isTouchSequence && this._lastTouchEventTime && this._lastTouchEventTime + MOUSE_EVENT_IGNORE_TIME > Date.now(); + const isFakeEvent = isTouchSequence ? isFakeTouchstartFromScreenReader(event as TouchEvent) : + isFakeMousedownFromScreenReader(event as MouseEvent); // 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 @@ -866,7 +872,7 @@ export class DragRef { } // Abort if the user is already dragging or is using a mouse button other than the primary one. - if (isDragging || isAuxiliaryMouseButton || isSyntheticEvent) { + if (isDragging || isAuxiliaryMouseButton || isSyntheticEvent || isFakeEvent) { return; }