Skip to content

Commit a05aafe

Browse files
crisbetojelbourn
authored andcommitted
fix(drag-drop): add fallback if the placeholder transition doesn't complete (#12121)
Since a lot of functionality depends on the animation completing, these changes add a fallback for the case where there's a transition on the preview, but it doesn't complete for some reason (e.g. it being too short). Currently clicking rapidly on the drag handle can cause it to get stuck and not complete the drop sequence correctly.
1 parent 2f9b51f commit a05aafe

File tree

2 files changed

+49
-5
lines changed

2 files changed

+49
-5
lines changed

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
Provider,
1010
ViewEncapsulation,
1111
} from '@angular/core';
12-
import {TestBed, ComponentFixture, fakeAsync, flush} from '@angular/core/testing';
12+
import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing';
1313
import {DragDropModule} from './drag-drop-module';
1414
import {dispatchMouseEvent, dispatchTouchEvent} from '@angular/cdk/testing';
1515
import {Directionality} from '@angular/cdk/bidi';
@@ -403,6 +403,36 @@ describe('CdkDrag', () => {
403403
.toBe('rtl', 'Expected preview element to inherit the directionality.');
404404
}));
405405

406+
it('should remove the preview if its `transitionend` event timed out', fakeAsync(() => {
407+
const fixture = createComponent(DraggableInDropZone);
408+
fixture.detectChanges();
409+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
410+
411+
dispatchMouseEvent(item, 'mousedown');
412+
fixture.detectChanges();
413+
414+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
415+
416+
// Add a duration since the tests won't include one.
417+
preview.style.transitionDuration = '500ms';
418+
419+
// Move somewhere so the draggable doesn't exit immediately.
420+
dispatchTouchEvent(document, 'mousemove', 50, 50);
421+
fixture.detectChanges();
422+
423+
dispatchMouseEvent(document, 'mouseup');
424+
fixture.detectChanges();
425+
tick(250);
426+
427+
expect(preview.parentNode)
428+
.toBeTruthy('Expected preview to be in the DOM mid-way through the transition');
429+
430+
tick(500);
431+
432+
expect(preview.parentNode)
433+
.toBeFalsy('Expected preview to be removed from the DOM if the transition timed out');
434+
}));
435+
406436
it('should create a placeholder element while the item is dragged', fakeAsync(() => {
407437
const fixture = createComponent(DraggableInDropZone);
408438
fixture.detectChanges();

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -436,21 +436,26 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
436436
// we need to trigger a style recalculation in order for the `cdk-drag-animating` class to
437437
// apply its style, we take advantage of the available info to figure out whether we need to
438438
// bind the event in the first place.
439-
const duration = getComputedStyle(this._preview).getPropertyValue('transition-duration');
439+
const duration = this._getTransitionDurationInMs(this._preview);
440440

441-
if (parseFloat(duration) === 0) {
441+
if (duration === 0) {
442442
return Promise.resolve();
443443
}
444444

445445
return this._ngZone.runOutsideAngular(() => {
446446
return new Promise(resolve => {
447-
const handler = (event: Event) => {
448-
if (event.target === this._preview) {
447+
const handler = (event: TransitionEvent) => {
448+
if (!event || event.target === this._preview) {
449449
this._preview.removeEventListener('transitionend', handler);
450450
resolve();
451+
clearTimeout(timeout);
451452
}
452453
};
453454

455+
// If a transition is short enough, the browser might not fire the `transitionend` event.
456+
// Since we know how long it's supposed to take, add a timeout with a 50% buffer that'll
457+
// fire if the transition hasn't completed when it was supposed to.
458+
const timeout = setTimeout(handler, duration * 1.5);
454459
this._preview.addEventListener('transitionend', handler);
455460
});
456461
});
@@ -551,6 +556,15 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
551556
this._document.addEventListener(isTouchEvent ? 'touchend' : 'mouseup', this._pointerUp);
552557
});
553558
}
559+
560+
/** Gets the `transition-duration` of an element in milliseconds. */
561+
private _getTransitionDurationInMs(element: HTMLElement): number {
562+
const rawDuration = getComputedStyle(element).getPropertyValue('transition-duration');
563+
564+
// Some browsers will return it in seconds, whereas others will return milliseconds.
565+
const multiplier = rawDuration.toLowerCase().indexOf('ms') > -1 ? 1 : 1000;
566+
return parseFloat(rawDuration) * multiplier;
567+
}
554568
}
555569

556570
/** Point on the page or within an element. */

0 commit comments

Comments
 (0)