Skip to content

Commit 738f10c

Browse files
Aboisierjelbourn
authored andcommitted
fix(drag-drop): fix drag start delay behavior to allow scrolling (#16228)
The current implementation of the drag start delay does not allow scrolling on mobile devices. Instead, the draggable element gets teleported to the cursor once the delay is elapsed. In order to handle this use case, we cancel the drag sequence if the cursor moves before the drag start delay is elapsed and we disable native drag interactions only when the drag sequence is started instead of when it is initialized. The drag start delay was also integrated to the drag drop demo. Fixes #16224
1 parent 1540391 commit 738f10c

File tree

5 files changed

+100
-16
lines changed

5 files changed

+100
-16
lines changed

src/cdk/drag-drop/directives/drag.spec.ts

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -604,14 +604,38 @@ describe('CdkDrag', () => {
604604
const dragElement = fixture.componentInstance.dragElement.nativeElement;
605605
const styles = dragElement.style;
606606

607-
expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none');
607+
expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy();
608608

609609
fixture.componentInstance.dragInstance.disabled = true;
610610
fixture.detectChanges();
611611

612612
expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy();
613613
}));
614614

615+
it('should enable native drag interactions if not dragging', fakeAsync(() => {
616+
const fixture = createComponent(StandaloneDraggable);
617+
fixture.detectChanges();
618+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
619+
const styles = dragElement.style;
620+
621+
expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy();
622+
}));
623+
624+
it('should disable native drag interactions if dragging', fakeAsync(() => {
625+
const fixture = createComponent(StandaloneDraggable);
626+
fixture.detectChanges();
627+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
628+
const styles = dragElement.style;
629+
630+
expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy();
631+
632+
startDraggingViaMouse(fixture, dragElement);
633+
dispatchMouseEvent(document, 'mousemove', 50, 100);
634+
fixture.detectChanges();
635+
636+
expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none');
637+
}));
638+
615639
it('should stop propagation for the drag sequence start event', fakeAsync(() => {
616640
const fixture = createComponent(StandaloneDraggable);
617641
fixture.detectChanges();
@@ -765,7 +789,7 @@ describe('CdkDrag', () => {
765789
}).toThrowError(/^cdkDrag must be attached to an element node/);
766790
}));
767791

768-
it('should allow for the dragging sequence to be delayed', fakeAsync(() => {
792+
it('should cancel drag if the mouse moves before the delay is elapsed', fakeAsync(() => {
769793
// We can't use Jasmine's `clock` because Zone.js interferes with it.
770794
spyOn(Date, 'now').and.callFake(() => currentTime);
771795
let currentTime = 0;
@@ -780,13 +804,52 @@ describe('CdkDrag', () => {
780804
startDraggingViaMouse(fixture, dragElement);
781805
currentTime += 750;
782806
dispatchMouseEvent(document, 'mousemove', 50, 100);
807+
currentTime += 500;
783808
fixture.detectChanges();
784809

785810
expect(dragElement.style.transform)
786-
.toBeFalsy('Expected element not to be moved if the drag timeout has not passed.');
811+
.toBeFalsy('Expected element not to be moved if the mouse moved before the delay.');
812+
}));
787813

788-
// The first `mousemove` here starts the sequence and the second one moves the element.
814+
it('should enable native drag interactions if mouse moves before the delay', fakeAsync(() => {
815+
// We can't use Jasmine's `clock` because Zone.js interferes with it.
816+
spyOn(Date, 'now').and.callFake(() => currentTime);
817+
let currentTime = 0;
818+
819+
const fixture = createComponent(StandaloneDraggable);
820+
fixture.componentInstance.dragStartDelay = 1000;
821+
fixture.detectChanges();
822+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
823+
const styles = dragElement.style;
824+
825+
expect(dragElement.style.transform).toBeFalsy('Expected element not to be moved by default.');
826+
827+
startDraggingViaMouse(fixture, dragElement);
828+
currentTime += 750;
829+
dispatchMouseEvent(document, 'mousemove', 50, 100);
789830
currentTime += 500;
831+
fixture.detectChanges();
832+
833+
expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy();
834+
}));
835+
836+
it('should allow dragging after the drag start delay is elapsed', fakeAsync(() => {
837+
// We can't use Jasmine's `clock` because Zone.js interferes with it.
838+
spyOn(Date, 'now').and.callFake(() => currentTime);
839+
let currentTime = 0;
840+
841+
const fixture = createComponent(StandaloneDraggable);
842+
fixture.componentInstance.dragStartDelay = 500;
843+
fixture.detectChanges();
844+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
845+
846+
expect(dragElement.style.transform).toBeFalsy('Expected element not to be moved by default.');
847+
848+
dispatchMouseEvent(dragElement, 'mousedown');
849+
fixture.detectChanges();
850+
currentTime += 750;
851+
852+
// The first `mousemove` here starts the sequence and the second one moves the element.
790853
dispatchMouseEvent(document, 'mousemove', 50, 100);
791854
dispatchMouseEvent(document, 'mousemove', 50, 100);
792855
fixture.detectChanges();
@@ -801,22 +864,17 @@ describe('CdkDrag', () => {
801864
let currentTime = 0;
802865

803866
const fixture = createComponent(StandaloneDraggable);
804-
fixture.componentInstance.dragStartDelay = '1000';
867+
fixture.componentInstance.dragStartDelay = '500';
805868
fixture.detectChanges();
806869
const dragElement = fixture.componentInstance.dragElement.nativeElement;
807870

808871
expect(dragElement.style.transform).toBeFalsy('Expected element not to be moved by default.');
809872

810-
startDraggingViaMouse(fixture, dragElement);
811-
currentTime += 750;
812-
dispatchMouseEvent(document, 'mousemove', 50, 100);
873+
dispatchMouseEvent(dragElement, 'mousedown');
813874
fixture.detectChanges();
814-
815-
expect(dragElement.style.transform)
816-
.toBeFalsy('Expected element not to be moved if the drag timeout has not passed.');
875+
currentTime += 750;
817876

818877
// The first `mousemove` here starts the sequence and the second one moves the element.
819-
currentTime += 500;
820878
dispatchMouseEvent(document, 'mousemove', 50, 100);
821879
dispatchMouseEvent(document, 'mousemove', 50, 100);
822880
fixture.detectChanges();

src/cdk/drag-drop/drag-ref.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,13 @@ export class DragRef<T = any> {
523523
// direction. Note that this is preferrable over doing something like `skip(minimumDistance)`
524524
// in the `pointerMove` subscription, because we're not guaranteed to have one move event
525525
// per pixel of movement (e.g. if the user moves their pointer quickly).
526-
if (isOverThreshold && (Date.now() >= this._dragStartTime + (this.dragStartDelay || 0))) {
526+
if (isOverThreshold) {
527+
const isDelayElapsed = Date.now() >= this._dragStartTime + (this.dragStartDelay || 0);
528+
if (!isDelayElapsed) {
529+
this._endDragSequence(event);
530+
return;
531+
}
532+
527533
// Prevent other drag sequences from starting while something in the container is still
528534
// being dragged. This can happen while we're waiting for the drop animation to finish
529535
// and can cause errors, because some elements might still be moving around.
@@ -586,6 +592,14 @@ export class DragRef<T = any> {
586592

587593
/** Handler that is invoked when the user lifts their pointer up, after initiating a drag. */
588594
private _pointerUp = (event: MouseEvent | TouchEvent) => {
595+
this._endDragSequence(event);
596+
}
597+
598+
/**
599+
* Clears subscriptions and stops the dragging sequence.
600+
* @param event Browser event object that ended the sequence.
601+
*/
602+
private _endDragSequence(event: MouseEvent | TouchEvent) {
589603
// Note that here we use `isDragging` from the service, rather than from `this`.
590604
// The difference is that the one from the service reflects whether a dragging sequence
591605
// has been initiated, whereas the one on `this` includes whether the user has passed
@@ -639,6 +653,8 @@ export class DragRef<T = any> {
639653
this._lastTouchEventTime = Date.now();
640654
}
641655

656+
this._toggleNativeDragInteractions();
657+
642658
if (this._dropContainer) {
643659
const element = this._rootElement;
644660

@@ -701,7 +717,6 @@ export class DragRef<T = any> {
701717
rootElement.style.webkitTapHighlightColor = 'transparent';
702718
}
703719

704-
this._toggleNativeDragInteractions();
705720
this._hasStartedDragging = this._hasMoved = false;
706721
this._initialContainer = this._dropContainer!;
707722

@@ -1016,7 +1031,7 @@ export class DragRef<T = any> {
10161031
return;
10171032
}
10181033

1019-
const shouldEnable = this.disabled || this._handles.length > 0;
1034+
const shouldEnable = this._handles.length > 0 || !this.isDragging();
10201035

10211036
if (shouldEnable !== this._nativeInteractionsEnabled) {
10221037
this._nativeInteractionsEnabled = shouldEnable;

src/dev-app/drag-drop/drag-drop-demo-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {NgModule} from '@angular/core';
1212
import {FormsModule} from '@angular/forms';
1313
import {MatFormFieldModule} from '@angular/material/form-field';
1414
import {MatIconModule} from '@angular/material/icon';
15+
import {MatInputModule} from '@angular/material/input';
1516
import {MatSelectModule} from '@angular/material/select';
1617
import {RouterModule} from '@angular/router';
1718
import {DragAndDropDemo} from './drag-drop-demo';
@@ -23,6 +24,7 @@ import {DragAndDropDemo} from './drag-drop-demo';
2324
FormsModule,
2425
MatFormFieldModule,
2526
MatIconModule,
27+
MatInputModule,
2628
MatSelectModule,
2729
RouterModule.forChild([{path: '', component: DragAndDropDemo}]),
2830
],

src/dev-app/drag-drop/drag-drop-demo.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ <h2>Horizontal list</h2>
4848

4949
<div class="demo-list">
5050
<h2>Free dragging</h2>
51-
<div cdkDrag class="demo-free-draggable" [cdkDragLockAxis]="axisLock">Drag me around</div>
51+
<div cdkDrag class="demo-free-draggable" [cdkDragLockAxis]="axisLock" [cdkDragStartDelay]="dragStartDelay">Drag me around</div>
5252
</div>
5353

5454
<div>
@@ -69,3 +69,11 @@ <h2>Axis locking</h2>
6969
</mat-select>
7070
</mat-form-field>
7171
</div>
72+
73+
<div>
74+
<h2>Drag start delay</h2>
75+
76+
<mat-form-field>
77+
<input matInput placeholder="Drag start delay" value="0" [(ngModel)]="dragStartDelay">
78+
</mat-form-field>
79+
</div>

src/dev-app/drag-drop/drag-drop-demo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag
2121
})
2222
export class DragAndDropDemo {
2323
axisLock: 'x' | 'y';
24+
dragStartDelay = 0;
2425
todo = [
2526
'Go out for Lunch',
2627
'Make a cool app',

0 commit comments

Comments
 (0)