Skip to content

feat(drag-drop): add support for automatic scrolling #16382

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 1 commit 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
1 change: 1 addition & 0 deletions src/cdk/drag-drop/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ ng_test_library(
deps = [
":drag-drop",
"//src/cdk/bidi",
"//src/cdk/scrolling",
"//src/cdk/testing",
"@npm//@angular/common",
"@npm//rxjs",
Expand Down
497 changes: 468 additions & 29 deletions src/cdk/drag-drop/directives/drag.spec.ts

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/cdk/drag-drop/directives/drop-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ export class CdkDropList<T = any> implements CdkDropListContainer, AfterContentI
@Input('cdkDropListEnterPredicate')
enterPredicate: (drag: CdkDrag, drop: CdkDropList) => boolean = () => true

/** Whether to auto-scroll the view when the user moves their pointer close to the edges. */
@Input('cdkDropListAutoScrollDisabled')
autoScrollDisabled: boolean = false;

/** Emits when the user drops an item inside the container. */
@Output('cdkDropListDropped')
dropped: EventEmitter<CdkDragDrop<T, any>> = new EventEmitter<CdkDragDrop<T, any>>();
Expand Down Expand Up @@ -298,6 +302,7 @@ export class CdkDropList<T = any> implements CdkDropListContainer, AfterContentI
ref.disabled = this.disabled;
ref.lockAxis = this.lockAxis;
ref.sortingDisabled = this.sortingDisabled;
ref.autoScrollDisabled = this.autoScrollDisabled;
ref
.connectedTo(siblings.filter(drop => drop && drop !== this).map(list => list._dropListRef))
.withOrientation(this.orientation);
Expand Down
31 changes: 21 additions & 10 deletions src/cdk/drag-drop/drag-drop-registry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ describe('DragDropRegistry', () => {
pointerMoveSubscription.unsubscribe();
});

it('should not emit pointer events when dragging is over (mutli touch)', () => {
it('should not emit pointer events when dragging is over (multi touch)', () => {
const firstItem = testComponent.dragItems.first;

// First finger down
Expand Down Expand Up @@ -211,15 +211,6 @@ describe('DragDropRegistry', () => {
expect(event.defaultPrevented).toBe(true);
});

it('should not prevent the default `wheel` actions when nothing is being dragged', () => {
expect(dispatchFakeEvent(document, 'wheel').defaultPrevented).toBe(false);
});

it('should prevent the default `wheel` action when an item is being dragged', () => {
registry.startDragging(testComponent.dragItems.first, createMouseEvent('mousedown'));
expect(dispatchFakeEvent(document, 'wheel').defaultPrevented).toBe(true);
});

it('should not prevent the default `selectstart` actions when nothing is being dragged', () => {
expect(dispatchFakeEvent(document, 'selectstart').defaultPrevented).toBe(false);
});
Expand All @@ -229,6 +220,26 @@ describe('DragDropRegistry', () => {
expect(dispatchFakeEvent(document, 'selectstart').defaultPrevented).toBe(true);
});

it('should dispatch `scroll` events if the viewport is scrolled while dragging', () => {
const spy = jasmine.createSpy('scroll spy');
const subscription = registry.scroll.subscribe(spy);

registry.startDragging(testComponent.dragItems.first, createMouseEvent('mousedown'));
dispatchFakeEvent(document, 'scroll');

expect(spy).toHaveBeenCalled();
subscription.unsubscribe();
});

it('should not dispatch `scroll` events when not dragging', () => {
const spy = jasmine.createSpy('scroll spy');
const subscription = registry.scroll.subscribe(spy);

dispatchFakeEvent(document, 'scroll');

expect(spy).not.toHaveBeenCalled();
subscription.unsubscribe();
});

});

Expand Down
15 changes: 6 additions & 9 deletions src/cdk/drag-drop/drag-drop-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export class DragDropRegistry<I, C extends {id: string}> implements OnDestroy {
*/
readonly pointerUp: Subject<TouchEvent | MouseEvent> = new Subject<TouchEvent | MouseEvent>();

/** Emits when the viewport has been scrolled while the user is dragging an item. */
readonly scroll: Subject<Event> = new Subject<Event>();

constructor(
private _ngZone: NgZone,
@Inject(DOCUMENT) _document: any) {
Expand Down Expand Up @@ -136,6 +139,9 @@ export class DragDropRegistry<I, C extends {id: string}> implements OnDestroy {
handler: (e: Event) => this.pointerUp.next(e as TouchEvent | MouseEvent),
options: true
})
.set('scroll', {
handler: (e: Event) => this.scroll.next(e)
})
// Preventing the default action on `mousemove` isn't enough to disable text selection
// on Safari so we need to prevent the selection event as well. Alternatively this can
// be done by setting `user-select: none` on the `body`, however it has causes a style
Expand All @@ -145,15 +151,6 @@ export class DragDropRegistry<I, C extends {id: string}> implements OnDestroy {
options: activeCapturingEventOptions
});

// TODO(crisbeto): prevent mouse wheel scrolling while
// dragging until we've set up proper scroll handling.
if (!isTouchEvent) {
this._globalListeners.set('wheel', {
handler: this._preventDefaultWhileDragging,
options: activeCapturingEventOptions
});
}

this._ngZone.runOutsideAngular(() => {
this._globalListeners.forEach((config, name) => {
this._document.addEventListener(name, config.handler, config.options);
Expand Down
3 changes: 2 additions & 1 deletion src/cdk/drag-drop/drag-drop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class DragDrop {
* @param element Element to which to attach the drop list functionality.
*/
createDropList<T = any>(element: ElementRef<HTMLElement> | HTMLElement): DropListRef<T> {
return new DropListRef<T>(element, this._dragDropRegistry, this._document);
return new DropListRef<T>(element, this._dragDropRegistry, this._document, this._ngZone,
this._viewportRuler);
}
}
36 changes: 26 additions & 10 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {Direction} from '@angular/cdk/bidi';
import {normalizePassiveListenerOptions} from '@angular/cdk/platform';
import {coerceBooleanProperty, coerceElement} from '@angular/cdk/coercion';
import {Subscription, Subject, Observable} from 'rxjs';
import {startWith} from 'rxjs/operators';
import {DropListRefInternal as DropListRef} from './drop-list-ref';
import {DragDropRegistry} from './drag-drop-registry';
import {extendStyles, toggleNativeDragInteractions} from './drag-styling';
Expand Down Expand Up @@ -46,7 +47,6 @@ const activeEventListenerOptions = normalizePassiveListenerOptions({passive: fal
*/
const MOUSE_EVENT_IGNORE_TIME = 800;

// TODO(crisbeto): add auto-scrolling functionality.
// TODO(crisbeto): add an API for moving a draggable up/down the
// list programmatically. Useful for keyboard controls.

Expand Down Expand Up @@ -155,6 +155,9 @@ export class DragRef<T = any> {
/** Subscription to the event that is dispatched when the user lifts their pointer. */
private _pointerUpSubscription = Subscription.EMPTY;

/** Subscription to the viewport being scrolled. */
private _scrollSubscription = Subscription.EMPTY;

/**
* Time at which the last touch event occurred. Used to avoid firing the same
* events multiple times on touch devices where the browser will fire a fake
Expand Down Expand Up @@ -446,10 +449,20 @@ export class DragRef<T = any> {
return this;
}

/** Updates the item's sort order based on the last-known pointer position. */
_sortFromLastPointerPosition() {
const position = this._pointerPositionAtLastDirectionChange;

if (position && this._dropContainer) {
this._updateActiveDropContainer(position);
}
}

/** Unsubscribes from the global subscriptions. */
private _removeSubscriptions() {
this._pointerMoveSubscription.unsubscribe();
this._pointerUpSubscription.unsubscribe();
this._scrollSubscription.unsubscribe();
}

/** Destroys the preview element and its ViewRef. */
Expand Down Expand Up @@ -593,7 +606,14 @@ export class DragRef<T = any> {

this.released.next({source: this});

if (!this._dropContainer) {
if (this._dropContainer) {
// Stop scrolling immediately, instead of waiting for the animation to finish.
this._dropContainer._stopScrolling();
this._animatePreviewToPlaceholder().then(() => {
this._cleanupDragArtifacts(event);
this._dragDropRegistry.stopDragging(this);
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense for stopDragging to cause _cleanupDragArtifacts in itself?

Copy link
Member Author

Choose a reason for hiding this comment

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

_cleanupDragArtifacts is private to the DragRef whereas stopDragging is in the DragDropRegistry. Also this logic isn't new, I just moved it around so it's easier to follow.

});
} else {
// Convert the active transform into a passive one. This means that next time
// the user starts dragging the item, its position will be calculated relatively
// to the new passive transform.
Expand All @@ -606,13 +626,7 @@ export class DragRef<T = any> {
});
});
this._dragDropRegistry.stopDragging(this);
return;
}

this._animatePreviewToPlaceholder().then(() => {
this._cleanupDragArtifacts(event);
this._dragDropRegistry.stopDragging(this);
});
}

/** Starts the dragging sequence. */
Expand Down Expand Up @@ -695,8 +709,9 @@ export class DragRef<T = any> {
this._removeSubscriptions();
this._pointerMoveSubscription = this._dragDropRegistry.pointerMove.subscribe(this._pointerMove);
this._pointerUpSubscription = this._dragDropRegistry.pointerUp.subscribe(this._pointerUp);

this._scrollPosition = this._viewportRuler.getViewportScrollPosition();
this._scrollSubscription = this._dragDropRegistry.scroll.pipe(startWith(null)).subscribe(() => {
this._scrollPosition = this._viewportRuler.getViewportScrollPosition();
});

if (this._boundaryElement) {
this._boundaryRect = this._boundaryElement.getBoundingClientRect();
Expand Down Expand Up @@ -789,6 +804,7 @@ export class DragRef<T = any> {
});
}

this._dropContainer!._startScrollingIfNecessary(x, y);
this._dropContainer!._sortItem(this, x, y, this._pointerDirectionDelta);
this._preview.style.transform =
getTransform(x - this._pickupPositionInElement.x, y - this._pickupPositionInElement.y);
Expand Down
Loading