Skip to content

fix(drag-drop): fix drag start delay behavior to allow scrolling (#16224) #16228

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 70 additions & 12 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,14 +602,38 @@ describe('CdkDrag', () => {
const dragElement = fixture.componentInstance.dragElement.nativeElement;
const styles = dragElement.style;

expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none');
expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy();

fixture.componentInstance.dragInstance.disabled = true;
fixture.detectChanges();

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

it('should enable native drag interactions if not dragging', fakeAsync(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the old test case is still valid. Should we have one that checks when it's disabled and another one if not dragging?

Copy link
Contributor Author

@Aboisier Aboisier Jun 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense! I'll add it back. I foresee only one small tweak I'm going to have to do to the test; the webkitUserDrag will now be falsy by default. This is to allow scrolling.

const fixture = createComponent(StandaloneDraggable);
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;
const styles = dragElement.style;

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

it('should disable native drag interactions if dragging', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggable);
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;
const styles = dragElement.style;

expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy();

startDraggingViaMouse(fixture, dragElement);
dispatchMouseEvent(document, 'mousemove', 50, 100);
fixture.detectChanges();

expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none');
}));

it('should stop propagation for the drag sequence start event', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggable);
fixture.detectChanges();
Expand Down Expand Up @@ -762,7 +786,7 @@ describe('CdkDrag', () => {
}).toThrowError(/^cdkDrag must be attached to an element node/);
}));

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

expect(dragElement.style.transform)
.toBeFalsy('Expected element not to be moved if the drag timeout has not passed.');
.toBeFalsy('Expected element not to be moved if the mouse moved before the delay.');
}));

// The first `mousemove` here starts the sequence and the second one moves the element.
it('should enable native drag interactions if mouse moves before the delay', fakeAsync(() => {
// We can't use Jasmine's `clock` because Zone.js interferes with it.
spyOn(Date, 'now').and.callFake(() => currentTime);
let currentTime = 0;

const fixture = createComponent(StandaloneDraggable);
fixture.componentInstance.dragStartDelay = 1000;
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;
const styles = dragElement.style;

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

startDraggingViaMouse(fixture, dragElement);
currentTime += 750;
dispatchMouseEvent(document, 'mousemove', 50, 100);
currentTime += 500;
fixture.detectChanges();

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

it('should allow dragging after the drag start delay is elapsed', fakeAsync(() => {
// We can't use Jasmine's `clock` because Zone.js interferes with it.
spyOn(Date, 'now').and.callFake(() => currentTime);
let currentTime = 0;

const fixture = createComponent(StandaloneDraggable);
fixture.componentInstance.dragStartDelay = 500;
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;

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

dispatchMouseEvent(dragElement, 'mousedown');
fixture.detectChanges();
currentTime += 750;

// The first `mousemove` here starts the sequence and the second one moves the element.
dispatchMouseEvent(document, 'mousemove', 50, 100);
dispatchMouseEvent(document, 'mousemove', 50, 100);
fixture.detectChanges();
Expand All @@ -798,22 +861,17 @@ describe('CdkDrag', () => {
let currentTime = 0;

const fixture = createComponent(StandaloneDraggable);
fixture.componentInstance.dragStartDelay = '1000';
fixture.componentInstance.dragStartDelay = '500';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes shouldn't have an effect on this test. Why did this need to be reduced?

Copy link
Contributor Author

@Aboisier Aboisier Jun 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test used to:

  1. Click (T 0)
  2. Move the mouse (T 750)
  3. Expect that the element did not move (T 750)
  4. Move the mouse again (T 1250)
  5. Expect the object to have moved (T 1250)

This did not reflect the new drag start delay behaviour I am suggesting. The first mouse move, because it is performed before the drag start delay, now cancels the drag sequence. The moving before the delay is elapsed is tested here. This test ensures that moving after the delay is elapsed moves the element.

fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;

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

startDraggingViaMouse(fixture, dragElement);
currentTime += 750;
dispatchMouseEvent(document, 'mousemove', 50, 100);
dispatchMouseEvent(dragElement, 'mousedown');
fixture.detectChanges();

expect(dragElement.style.transform)
.toBeFalsy('Expected element not to be moved if the drag timeout has not passed.');
currentTime += 750;

// The first `mousemove` here starts the sequence and the second one moves the element.
currentTime += 500;
dispatchMouseEvent(document, 'mousemove', 50, 100);
dispatchMouseEvent(document, 'mousemove', 50, 100);
fixture.detectChanges();
Expand Down
21 changes: 18 additions & 3 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,13 @@ export class DragRef<T = any> {
// direction. Note that this is preferrable over doing something like `skip(minimumDistance)`
// in the `pointerMove` subscription, because we're not guaranteed to have one move event
// per pixel of movement (e.g. if the user moves their pointer quickly).
if (isOverThreshold && (Date.now() >= this._dragStartTime + (this.dragStartDelay || 0))) {
if (isOverThreshold) {
const isDelayElapsed = Date.now() >= this._dragStartTime + (this.dragStartDelay || 0);
if (!isDelayElapsed) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm following it correctly, it seems like we'll keep starting and ending dragging sequences until the delay has elapsed. Don't we want to not start the sequence in the first place? This feels like it could lead some issues like events being dispatched when they shouldn't be.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the pointer moves before the drag start delay has elapsed, then the drag sequence is ended. Since ending the sequence removes the subscription to pointermove, this method won't be called until the user releases the click and initializes a new sequence. This means the sequence will not be started/ended a bunch of times. At least, that's my understanding. I might be missing something here!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I tried it out and it doesn't seem to be an issue.

this._endDragSequence(event);
return;
}

// Prevent other drag sequences from starting while something in the container is still
// being dragged. This can happen while we're waiting for the drop animation to finish
// and can cause errors, because some elements might still be moving around.
Expand Down Expand Up @@ -572,6 +578,14 @@ export class DragRef<T = any> {

/** Handler that is invoked when the user lifts their pointer up, after initiating a drag. */
private _pointerUp = (event: MouseEvent | TouchEvent) => {
this._endDragSequence(event);
}

/**
* Clears subscriptions and stops the dragging sequence.
* @param event Browser event object that ended the sequence.
*/
private _endDragSequence(event: MouseEvent | TouchEvent) {
// Note that here we use `isDragging` from the service, rather than from `this`.
// The difference is that the one from the service reflects whether a dragging sequence
// has been initiated, whereas the one on `this` includes whether the user has passed
Expand Down Expand Up @@ -624,6 +638,8 @@ export class DragRef<T = any> {
this._lastTouchEventTime = Date.now();
}

this._toggleNativeDragInteractions();

if (this._dropContainer) {
const element = this._rootElement;

Expand Down Expand Up @@ -686,7 +702,6 @@ export class DragRef<T = any> {
rootElement.style.webkitTapHighlightColor = 'transparent';
}

this._toggleNativeDragInteractions();
this._hasStartedDragging = this._hasMoved = false;
this._initialContainer = this._dropContainer!;

Expand Down Expand Up @@ -999,7 +1014,7 @@ export class DragRef<T = any> {
return;
}

const shouldEnable = this.disabled || this._handles.length > 0;
const shouldEnable = this._handles.length > 0 || !this.isDragging();

if (shouldEnable !== this._nativeInteractionsEnabled) {
this._nativeInteractionsEnabled = shouldEnable;
Expand Down
2 changes: 2 additions & 0 deletions src/dev-app/drag-drop/drag-drop-demo-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatSelectModule} from '@angular/material/select';
import {RouterModule} from '@angular/router';
import {DragAndDropDemo} from './drag-drop-demo';
Expand All @@ -23,6 +24,7 @@ import {DragAndDropDemo} from './drag-drop-demo';
FormsModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatSelectModule,
RouterModule.forChild([{path: '', component: DragAndDropDemo}]),
],
Expand Down
10 changes: 9 additions & 1 deletion src/dev-app/drag-drop/drag-drop-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ <h2>Horizontal list</h2>

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

<div>
Expand All @@ -69,3 +69,11 @@ <h2>Axis locking</h2>
</mat-select>
</mat-form-field>
</div>

<div>
<h2>Drag start delay</h2>

<mat-form-field>
<input matInput placeholder="Drag start delay" value="0" [(ngModel)]="dragStartDelay">
</mat-form-field>
</div>
1 change: 1 addition & 0 deletions src/dev-app/drag-drop/drag-drop-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag
})
export class DragAndDropDemo {
axisLock: 'x' | 'y';
dragStartDelay = 0;
todo = [
'Go out for Lunch',
'Make a cool app',
Expand Down