From 156361abb95c6da1192b327cd4e7cd9fd4703d0f Mon Sep 17 00:00:00 2001 From: crisbeto Date: Tue, 2 Jul 2019 06:21:38 +0200 Subject: [PATCH] feat(drag-drop): add support for automatic scrolling * Adds support for automatically scrolling either the list or the viewport when the user's cursor gets within a certain threshold of the edges (currently within 5% inside and outside). * Handles changes to the scroll position of both the list and the viewport while the user is dragging. Previous our positioning would break down and we'd emit incorrect data. * No longer blocks the mouse wheel scrolling while the user is dragging. * Allows the consumer to opt out of the automatic scrolling. Fixes #13588. --- src/cdk/drag-drop/BUILD.bazel | 1 + src/cdk/drag-drop/directives/drag.spec.ts | 497 +++++++++++++++++-- src/cdk/drag-drop/directives/drop-list.ts | 5 + src/cdk/drag-drop/drag-drop-registry.spec.ts | 31 +- src/cdk/drag-drop/drag-drop-registry.ts | 15 +- src/cdk/drag-drop/drag-drop.ts | 3 +- src/cdk/drag-drop/drag-ref.ts | 36 +- src/cdk/drag-drop/drop-list-ref.ts | 346 ++++++++++++- tools/public_api_guard/cdk/drag-drop.d.ts | 9 +- 9 files changed, 858 insertions(+), 85 deletions(-) diff --git a/src/cdk/drag-drop/BUILD.bazel b/src/cdk/drag-drop/BUILD.bazel index 3945ed74391a..1285650897d1 100644 --- a/src/cdk/drag-drop/BUILD.bazel +++ b/src/cdk/drag-drop/BUILD.bazel @@ -35,6 +35,7 @@ ng_test_library( deps = [ ":drag-drop", "//src/cdk/bidi", + "//src/cdk/scrolling", "//src/cdk/testing", "@npm//@angular/common", "@npm//rxjs", diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 885120435701..205bc35ab6ca 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -5,6 +5,7 @@ import { dispatchEvent, dispatchMouseEvent, dispatchTouchEvent, + dispatchFakeEvent, } from '@angular/cdk/testing'; import { AfterViewInit, @@ -22,6 +23,7 @@ import { } from '@angular/core'; import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing'; import {DOCUMENT} from '@angular/common'; +import {ViewportRuler} from '@angular/cdk/scrolling'; import {of as observableOf} from 'rxjs'; import {DragDropModule} from '../drag-drop-module'; @@ -1478,6 +1480,96 @@ describe('CdkDrag', () => { .toEqual(['Zero', 'One', 'Two', 'Three']); })); + it('should calculate the index if the list is scrolled while dragging', fakeAsync(() => { + const fixture = createComponent(DraggableInScrollableVerticalDropZone); + fixture.detectChanges(); + const dragItems = fixture.componentInstance.dragItems; + const firstItem = dragItems.first; + const thirdItemRect = dragItems.toArray()[2].element.nativeElement.getBoundingClientRect(); + const list = fixture.componentInstance.dropInstance.element.nativeElement; + + startDraggingViaMouse(fixture, firstItem.element.nativeElement); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); + fixture.detectChanges(); + + list.scrollTop = ITEM_HEIGHT * 10; + dispatchFakeEvent(list, 'scroll'); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + + expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); + + const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + + // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will + // go into an infinite loop trying to stringify the event, if the test fails. + expect(event).toEqual({ + previousIndex: 0, + currentIndex: 12, + item: firstItem, + container: fixture.componentInstance.dropInstance, + previousContainer: fixture.componentInstance.dropInstance, + isPointerOverContainer: jasmine.any(Boolean), + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} + }); + })); + + it('should calculate the index if the viewport is scrolled while dragging', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + + for (let i = 0; i < 200; i++) { + fixture.componentInstance.items.push({ + value: `Extra item ${i}`, + height: ITEM_HEIGHT, + margin: 0 + }); + } + + fixture.detectChanges(); + const dragItems = fixture.componentInstance.dragItems; + const firstItem = dragItems.first; + const thirdItemRect = dragItems.toArray()[2].element.nativeElement.getBoundingClientRect(); + + startDraggingViaMouse(fixture, firstItem.element.nativeElement); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1); + fixture.detectChanges(); + + scrollTo(0, ITEM_HEIGHT * 10); + dispatchFakeEvent(document, 'scroll'); + fixture.detectChanges(); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + + expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); + + const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + + // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will + // go into an infinite loop trying to stringify the event, if the test fails. + expect(event).toEqual({ + previousIndex: 0, + currentIndex: 12, + item: firstItem, + container: fixture.componentInstance.dropInstance, + previousContainer: fixture.componentInstance.dropInstance, + isPointerOverContainer: jasmine.any(Boolean), + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} + }); + + scrollTo(0, 0); + })); + it('should create a preview element while the item is dragged', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); @@ -2374,6 +2466,29 @@ describe('CdkDrag', () => { expect(preview.style.transform).toBe('translate3d(50px, 50px, 0px)'); })); + it('should keep the preview next to the trigger if the page was scrolled', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZoneWithCustomPreview); + fixture.detectChanges(); + const cleanup = makePageScrollable(); + const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + + startDraggingViaMouse(fixture, item, 50, 50); + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + expect(preview.style.transform).toBe('translate3d(50px, 50px, 0px)'); + + scrollTo(0, 500); + fixture.detectChanges(); + + // Move the pointer a bit so the preview has to reposition. + dispatchMouseEvent(document, 'mousemove', 55, 55); + fixture.detectChanges(); + + expect(preview.style.transform).toBe('translate3d(55px, 555px, 0px)'); + + cleanup(); + })); + it('should lock position inside a drop container along the x axis', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); fixture.detectChanges(); @@ -2656,6 +2771,272 @@ describe('CdkDrag', () => { .toBe(1, 'Expected only one item to continue to be dragged.'); })); + it('should should be able to disable auto-scrolling', fakeAsync(() => { + const fixture = createComponent(DraggableInScrollableVerticalDropZone); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const list = fixture.componentInstance.dropInstance.element.nativeElement; + const listRect = list.getBoundingClientRect(); + + fixture.componentInstance.dropInstance.autoScrollDisabled = true; + fixture.detectChanges(); + + expect(list.scrollTop).toBe(0); + + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', + listRect.left + listRect.width / 2, listRect.top + listRect.height); + fixture.detectChanges(); + tickAnimationFrames(20); + + expect(list.scrollTop).toBe(0); + })); + + it('should auto-scroll down if the user holds their pointer at bottom edge', fakeAsync(() => { + const fixture = createComponent(DraggableInScrollableVerticalDropZone); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const list = fixture.componentInstance.dropInstance.element.nativeElement; + const listRect = list.getBoundingClientRect(); + + expect(list.scrollTop).toBe(0); + + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', + listRect.left + listRect.width / 2, listRect.top + listRect.height); + fixture.detectChanges(); + tickAnimationFrames(20); + + expect(list.scrollTop).toBeGreaterThan(0); + })); + + it('should auto-scroll up if the user holds their pointer at top edge', fakeAsync(() => { + const fixture = createComponent(DraggableInScrollableVerticalDropZone); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const list = fixture.componentInstance.dropInstance.element.nativeElement; + const listRect = list.getBoundingClientRect(); + const initialScrollDistance = list.scrollTop = list.scrollHeight; + + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', listRect.left + listRect.width / 2, listRect.top); + fixture.detectChanges(); + tickAnimationFrames(20); + + expect(list.scrollTop).toBeLessThan(initialScrollDistance); + })); + + it('should auto-scroll right if the user holds their pointer at right edge', fakeAsync(() => { + const fixture = createComponent(DraggableInScrollableHorizontalDropZone); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const list = fixture.componentInstance.dropInstance.element.nativeElement; + const listRect = list.getBoundingClientRect(); + + expect(list.scrollLeft).toBe(0); + + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', listRect.left + listRect.width, + listRect.top + listRect.height / 2); + fixture.detectChanges(); + tickAnimationFrames(20); + + expect(list.scrollLeft).toBeGreaterThan(0); + })); + + it('should auto-scroll left if the user holds their pointer at left edge', fakeAsync(() => { + const fixture = createComponent(DraggableInScrollableHorizontalDropZone); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const list = fixture.componentInstance.dropInstance.element.nativeElement; + const listRect = list.getBoundingClientRect(); + const initialScrollDistance = list.scrollLeft = list.scrollWidth; + + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', listRect.left, listRect.top + listRect.height / 2); + fixture.detectChanges(); + tickAnimationFrames(20); + + expect(list.scrollLeft).toBeLessThan(initialScrollDistance); + })); + + it('should stop scrolling if the user moves their pointer away', fakeAsync(() => { + const fixture = createComponent(DraggableInScrollableVerticalDropZone); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const list = fixture.componentInstance.dropInstance.element.nativeElement; + const listRect = list.getBoundingClientRect(); + + expect(list.scrollTop).toBe(0); + + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', + listRect.left + listRect.width / 2, listRect.top + listRect.height); + fixture.detectChanges(); + tickAnimationFrames(20); + + const previousScrollTop = list.scrollTop; + expect(previousScrollTop).toBeGreaterThan(0); + + // Move the pointer away from the edge of the element. + dispatchMouseEvent(document, 'mousemove', + listRect.left + listRect.width / 2, listRect.top + listRect.height / 2); + fixture.detectChanges(); + tickAnimationFrames(20); + + expect(list.scrollTop).toBe(previousScrollTop); + })); + + it('should stop scrolling if the user stops dragging', fakeAsync(() => { + const fixture = createComponent(DraggableInScrollableVerticalDropZone); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const list = fixture.componentInstance.dropInstance.element.nativeElement; + const listRect = list.getBoundingClientRect(); + + expect(list.scrollTop).toBe(0); + + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', + listRect.left + listRect.width / 2, listRect.top + listRect.height); + fixture.detectChanges(); + tickAnimationFrames(20); + + const previousScrollTop = list.scrollTop; + expect(previousScrollTop).toBeGreaterThan(0); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + tickAnimationFrames(20); + + expect(list.scrollTop).toBe(previousScrollTop); + })); + + it('should auto-scroll viewport down if the pointer is close to bottom edge', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + + const cleanup = makePageScrollable(); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const viewportRuler: ViewportRuler = TestBed.get(ViewportRuler); + const viewportSize = viewportRuler.getViewportSize(); + + expect(viewportRuler.getViewportScrollPosition().top).toBe(0); + + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', viewportSize.width / 2, viewportSize.height); + fixture.detectChanges(); + tickAnimationFrames(20); + + expect(viewportRuler.getViewportScrollPosition().top).toBeGreaterThan(0); + + cleanup(); + })); + + it('should auto-scroll viewport up if the pointer is close to top edge', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + + const cleanup = makePageScrollable(); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const viewportRuler: ViewportRuler = TestBed.get(ViewportRuler); + const viewportSize = viewportRuler.getViewportSize(); + + scrollTo(0, viewportSize.height * 5); + const initialScrollDistance = viewportRuler.getViewportScrollPosition().top; + expect(initialScrollDistance).toBeGreaterThan(0); + + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', viewportSize.width / 2, 0); + fixture.detectChanges(); + tickAnimationFrames(20); + + expect(viewportRuler.getViewportScrollPosition().top).toBeLessThan(initialScrollDistance); + + cleanup(); + })); + + it('should auto-scroll viewport right if the pointer is near right edge', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + + const cleanup = makePageScrollable('horizontal'); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const viewportRuler: ViewportRuler = TestBed.get(ViewportRuler); + const viewportSize = viewportRuler.getViewportSize(); + + expect(viewportRuler.getViewportScrollPosition().left).toBe(0); + + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', viewportSize.width, viewportSize.height / 2); + fixture.detectChanges(); + tickAnimationFrames(20); + + expect(viewportRuler.getViewportScrollPosition().left).toBeGreaterThan(0); + + cleanup(); + })); + + it('should auto-scroll viewport left if the pointer is close to left edge', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + + const cleanup = makePageScrollable('horizontal'); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; + const viewportRuler: ViewportRuler = TestBed.get(ViewportRuler); + const viewportSize = viewportRuler.getViewportSize(); + + scrollTo(viewportSize.width * 5, 0); + const initialScrollDistance = viewportRuler.getViewportScrollPosition().left; + expect(initialScrollDistance).toBeGreaterThan(0); + + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', 0, viewportSize.height / 2); + fixture.detectChanges(); + tickAnimationFrames(20); + + expect(viewportRuler.getViewportScrollPosition().left).toBeLessThan(initialScrollDistance); + + cleanup(); + })); + + it('should auto-scroll the viewport, not the list, when the pointer is over the edge of ' + + 'both the list and the viewport', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + + const list = fixture.componentInstance.dropInstance.element.nativeElement; + const viewportRuler: ViewportRuler = TestBed.get(ViewportRuler); + const item = fixture.componentInstance.dragItems.first.element.nativeElement; + + // Position the list so that its top aligns with the viewport top. That way the pointer + // will both over its top edge and the viewport's. We use top instead of bottom, because + // bottom behaves weirdly when we run tests on mobile devices. + list.style.position = 'fixed'; + list.style.left = '50%'; + list.style.top = '0'; + list.style.margin = '0'; + + const listRect = list.getBoundingClientRect(); + const cleanup = makePageScrollable(); + + scrollTo(0, viewportRuler.getViewportSize().height * 5); + + const initialScrollDistance = viewportRuler.getViewportScrollPosition().top; + expect(initialScrollDistance).toBeGreaterThan(0); + expect(list.scrollTop).toBe(0); + + startDraggingViaMouse(fixture, item); + dispatchMouseEvent(document, 'mousemove', listRect.left + listRect.width / 2, 0); + fixture.detectChanges(); + tickAnimationFrames(20); + + expect(viewportRuler.getViewportScrollPosition().top).toBeLessThan(initialScrollDistance); + expect(list.scrollTop).toBe(0); + + cleanup(); + })); + }); describe('in a connected drop container', () => { @@ -3654,6 +4035,7 @@ class StandaloneDraggableWithMultipleHandles { const DROP_ZONE_FIXTURE_TEMPLATE = `
-
{{item.value}}
-
- ` + *ngFor="let item of items" + [style.width.px]="item.width" + [style.margin-right.px]="item.margin" + cdkDrag>{{item.value}}
+ +`; + +@Component({ + encapsulation: ViewEncapsulation.None, + styles: [HORIZONTAL_FIXTURE_STYLES], + template: HORIZONTAL_FIXTURE_TEMPLATE }) class DraggableInHorizontalDropZone { @ViewChildren(CdkDrag) dragItems: QueryList; @@ -3741,6 +4150,31 @@ class DraggableInHorizontalDropZone { }); } + +@Component({ + template: HORIZONTAL_FIXTURE_TEMPLATE, + + // Note that it needs a margin to ensure that it's not flush against the viewport + // edge which will cause the viewport to scroll, rather than the list. + styles: [HORIZONTAL_FIXTURE_STYLES, ` + .drop-list { + max-width: 300px; + margin: 10vw 0 0 10vw; + overflow: auto; + white-space: nowrap; + } + `] +}) +class DraggableInScrollableHorizontalDropZone extends DraggableInHorizontalDropZone { + constructor() { + super(); + + for (let i = 0; i < 60; i++) { + this.items.push({value: `Extra item ${i}`, width: ITEM_WIDTH, margin: 0}); + } + } +} + @Component({ template: `
@@ -4264,10 +4698,10 @@ function getElementSibligsByPosition(element: Element, direction: 'top' | 'left' * Adds a large element to the page in order to make it scrollable. * @returns Function that should be used to clean up after the test is done. */ -function makePageScrollable() { +function makePageScrollable(direction: 'vertical' | 'horizontal' = 'vertical') { const veryTallElement = document.createElement('div'); - veryTallElement.style.width = '100%'; - veryTallElement.style.height = '2000px'; + veryTallElement.style.width = direction === 'vertical' ? '100%' : '4000px'; + veryTallElement.style.height = direction === 'vertical' ? '2000px' : '5px'; document.body.appendChild(veryTallElement); return () => { @@ -4331,3 +4765,8 @@ function assertUpwardSorting(fixture: ComponentFixture, items: Element[]) { fixture.detectChanges(); flush(); } + +/** Ticks the specified amount of `requestAnimationFrame`-s. */ +function tickAnimationFrames(amount: number) { + tick(16.6 * amount); // Angular turns rAF calls into 16.6ms timeouts in tests. +} diff --git a/src/cdk/drag-drop/directives/drop-list.ts b/src/cdk/drag-drop/directives/drop-list.ts index 8a34037e0adc..c62a38cb8a5c 100644 --- a/src/cdk/drag-drop/directives/drop-list.ts +++ b/src/cdk/drag-drop/directives/drop-list.ts @@ -129,6 +129,10 @@ export class CdkDropList 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> = new EventEmitter>(); @@ -298,6 +302,7 @@ export class CdkDropList 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); diff --git a/src/cdk/drag-drop/drag-drop-registry.spec.ts b/src/cdk/drag-drop/drag-drop-registry.spec.ts index b7a62a2319ab..8af6d8407ae3 100644 --- a/src/cdk/drag-drop/drag-drop-registry.spec.ts +++ b/src/cdk/drag-drop/drag-drop-registry.spec.ts @@ -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 @@ -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); }); @@ -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(); + }); }); diff --git a/src/cdk/drag-drop/drag-drop-registry.ts b/src/cdk/drag-drop/drag-drop-registry.ts index 1151a9c60edd..7a46f667b696 100644 --- a/src/cdk/drag-drop/drag-drop-registry.ts +++ b/src/cdk/drag-drop/drag-drop-registry.ts @@ -56,6 +56,9 @@ export class DragDropRegistry implements OnDestroy { */ readonly pointerUp: Subject = new Subject(); + /** Emits when the viewport has been scrolled while the user is dragging an item. */ + readonly scroll: Subject = new Subject(); + constructor( private _ngZone: NgZone, @Inject(DOCUMENT) _document: any) { @@ -136,6 +139,9 @@ export class DragDropRegistry 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 @@ -145,15 +151,6 @@ export class DragDropRegistry 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); diff --git a/src/cdk/drag-drop/drag-drop.ts b/src/cdk/drag-drop/drag-drop.ts index b86466ee2c9f..a62b3a356ba0 100644 --- a/src/cdk/drag-drop/drag-drop.ts +++ b/src/cdk/drag-drop/drag-drop.ts @@ -47,6 +47,7 @@ export class DragDrop { * @param element Element to which to attach the drop list functionality. */ createDropList(element: ElementRef | HTMLElement): DropListRef { - return new DropListRef(element, this._dragDropRegistry, this._document); + return new DropListRef(element, this._dragDropRegistry, this._document, this._ngZone, + this._viewportRuler); } } diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index 34dc1c1c4fb3..4fe7a1890754 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -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'; @@ -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. @@ -155,6 +155,9 @@ export class DragRef { /** 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 @@ -446,10 +449,20 @@ export class DragRef { 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. */ @@ -593,7 +606,14 @@ export class DragRef { 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); + }); + } 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. @@ -606,13 +626,7 @@ export class DragRef { }); }); this._dragDropRegistry.stopDragging(this); - return; } - - this._animatePreviewToPlaceholder().then(() => { - this._cleanupDragArtifacts(event); - this._dragDropRegistry.stopDragging(this); - }); } /** Starts the dragging sequence. */ @@ -695,8 +709,9 @@ export class DragRef { 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(); @@ -789,6 +804,7 @@ export class DragRef { }); } + 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); diff --git a/src/cdk/drag-drop/drop-list-ref.ts b/src/cdk/drag-drop/drop-list-ref.ts index 68c9d5e56d65..21b9cf2760ff 100644 --- a/src/cdk/drag-drop/drop-list-ref.ts +++ b/src/cdk/drag-drop/drop-list-ref.ts @@ -6,15 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {ElementRef} from '@angular/core'; -import {DragDropRegistry} from './drag-drop-registry'; +import {ElementRef, NgZone} from '@angular/core'; import {Direction} from '@angular/cdk/bidi'; import {coerceElement} from '@angular/cdk/coercion'; -import {Subject} from 'rxjs'; +import {ViewportRuler} from '@angular/cdk/scrolling'; +import {Subject, Subscription, interval, animationFrameScheduler} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; import {moveItemInArray} from './drag-utils'; +import {DragDropRegistry} from './drag-drop-registry'; import {DragRefInternal as DragRef, Point} from './drag-ref'; - /** Counter used to generate unique ids for drop refs. */ let _uniqueIdCounter = 0; @@ -24,6 +25,18 @@ let _uniqueIdCounter = 0; */ const DROP_PROXIMITY_THRESHOLD = 0.05; +/** + * Proximity, as a ratio to width/height at which to start auto-scrolling the drop list or the + * viewport. The value comes from trying it out manually until it feels right. + */ +const SCROLL_PROXIMITY_THRESHOLD = 0.05; + +/** + * Number of pixels to scroll for each frame when auto-scrolling an element. + * The value comes from trying it out manually until it feels right. + */ +const AUTO_SCROLL_STEP = 2; + /** * Entry in the position cache for draggable items. * @docs-private @@ -37,6 +50,18 @@ interface CachedItemPosition { offset: number; } +/** Object holding the scroll position of something. */ +interface ScrollPosition { + top: number; + left: number; +} + +/** Vertical direction in which we can auto-scroll. */ +const enum AutoScrollVerticalDirection {NONE, UP, DOWN} + +/** Horizontal direction in which we can auto-scroll. */ +const enum AutoScrollHorizontalDirection {NONE, LEFT, RIGHT} + /** * Internal compile-time-only representation of a `DropListRef`. * Used to avoid circular import issues between the `DropListRef` and the `DragRef`. @@ -70,6 +95,12 @@ export class DropListRef { /** Locks the position of the draggable elements inside the container along the specified axis. */ lockAxis: 'x' | 'y'; + /** + * Whether auto-scrolling the view when the user + * moves their pointer close to the edges is disabled. + */ + autoScrollDisabled: boolean = false; + /** * Function that is used to determine whether an item * is allowed to be moved into a drop container. @@ -118,6 +149,12 @@ export class DropListRef { /** Cache of the dimensions of all the items inside the container. */ private _itemPositions: CachedItemPosition[] = []; + /** Keeps track of the container's scroll position. */ + private _scrollPosition: ScrollPosition = {top: 0, left: 0}; + + /** Keeps track of the scroll position of the viewport. */ + private _viewportScrollPosition: ScrollPosition = {top: 0, left: 0}; + /** Cached `ClientRect` of the drop list. */ private _clientRect: ClientRect; @@ -149,10 +186,31 @@ export class DropListRef { /** Layout direction of the drop list. */ private _direction: Direction = 'ltr'; + /** Subscription to the window being scrolled. */ + private _viewportScrollSubscription = Subscription.EMPTY; + + /** Vertical direction in which the list is currently scrolling. */ + private _verticalScrollDirection = AutoScrollVerticalDirection.NONE; + + /** Horizontal direction in which the list is currently scrolling. */ + private _horizontalScrollDirection = AutoScrollHorizontalDirection.NONE; + + /** Node that is being auto-scrolled. */ + private _scrollNode: HTMLElement | Window; + + /** Used to signal to the current auto-scroll sequence when to stop. */ + private _stopScrollTimers = new Subject(); + constructor( element: ElementRef | HTMLElement, private _dragDropRegistry: DragDropRegistry, - _document: any) { + _document: any, + /** + * @deprecated _ngZone and _viewportRuler parameters to be made required. + * @breaking-change 9.0.0 + */ + private _ngZone?: NgZone, + private _viewportRuler?: ViewportRuler) { _dragDropRegistry.registerDropContainer(this); this._document = _document; this.element = element instanceof ElementRef ? element.nativeElement : element; @@ -160,12 +218,16 @@ export class DropListRef { /** Removes the drop list functionality from the DOM element. */ dispose() { + this._stopScrolling(); + this._stopScrollTimers.complete(); + this._removeListeners(); this.beforeStarted.complete(); this.entered.complete(); this.exited.complete(); this.dropped.complete(); this.sorted.complete(); this._activeSiblings.clear(); + this._scrollNode = null!; this._dragDropRegistry.removeDropContainer(this); } @@ -176,10 +238,31 @@ export class DropListRef { /** Starts dragging an item. */ start(): void { + const element = coerceElement(this.element); this.beforeStarted.next(); this._isDragging = true; this._cacheItems(); this._siblings.forEach(sibling => sibling._startReceiving(this)); + this._removeListeners(); + + // @breaking-change 9.0.0 Remove check for _ngZone once it's marked as a required param. + if (this._ngZone) { + this._ngZone.runOutsideAngular(() => element.addEventListener('scroll', this._handleScroll)); + } else { + element.addEventListener('scroll', this._handleScroll); + } + + // @breaking-change 9.0.0 Remove check for _viewportRuler once it's marked as a required param. + if (this._viewportRuler) { + this._viewportScrollPosition = this._viewportRuler.getViewportScrollPosition(); + this._viewportScrollSubscription = this._dragDropRegistry.scroll.subscribe(() => { + if (this.isDragging()) { + const newPosition = this._viewportRuler!.getViewportScrollPosition(); + this._updateAfterScroll(this._viewportScrollPosition, newPosition.top, newPosition.left, + this._clientRect); + } + }); + } } /** @@ -419,9 +502,76 @@ export class DropListRef { }); } + /** + * Checks whether the user's pointer is close to the edges of either the + * viewport or the drop list and starts the auto-scroll sequence. + * @param pointerX User's pointer position along the x axis. + * @param pointerY User's pointer position along the y axis. + */ + _startScrollingIfNecessary(pointerX: number, pointerY: number) { + if (this.autoScrollDisabled) { + return; + } + + let scrollNode: HTMLElement | Window | undefined; + let verticalScrollDirection = AutoScrollVerticalDirection.NONE; + let horizontalScrollDirection = AutoScrollHorizontalDirection.NONE; + + // @breaking-change 9.0.0 Remove null check for _viewportRuler once it's a required parameter. + // Check whether we're in range to scroll the viewport. + if (this._viewportRuler) { + const {width, height} = this._viewportRuler.getViewportSize(); + const clientRect = {width, height, top: 0, right: width, bottom: height, left: 0}; + verticalScrollDirection = getVerticalScrollDirection(clientRect, pointerY); + horizontalScrollDirection = getHorizontalScrollDirection(clientRect, pointerX); + scrollNode = window; + } + + // If we couldn't find a scroll direction based on the + // window, try with the container, if the pointer is close by. + if (!verticalScrollDirection && !horizontalScrollDirection && + this._isPointerNearDropContainer(pointerX, pointerY)) { + verticalScrollDirection = getVerticalScrollDirection(this._clientRect, pointerY); + horizontalScrollDirection = getHorizontalScrollDirection(this._clientRect, pointerX); + scrollNode = coerceElement(this.element); + } + + // TODO(crisbeto): we also need to account for whether the view or element are scrollable in + // the first place. With the current approach we'll still try to scroll them, but it just + // won't do anything. The only case where this is relevant is that if we have a scrollable + // list close to the viewport edge where the viewport isn't scrollable. In this case the + // we'll be trying to scroll the viewport rather than the list. + + if (scrollNode && (verticalScrollDirection !== this._verticalScrollDirection || + horizontalScrollDirection !== this._horizontalScrollDirection || + scrollNode !== this._scrollNode)) { + this._verticalScrollDirection = verticalScrollDirection; + this._horizontalScrollDirection = horizontalScrollDirection; + this._scrollNode = scrollNode; + + if ((verticalScrollDirection || horizontalScrollDirection) && scrollNode) { + // @breaking-change 9.0.0 Remove null check for `_ngZone` once it is made required. + if (this._ngZone) { + this._ngZone.runOutsideAngular(this._startScrollInterval); + } else { + this._startScrollInterval(); + } + } else { + this._stopScrolling(); + } + } + } + + /** Stops any currently-running auto-scroll sequences. */ + _stopScrolling() { + this._stopScrollTimers.next(); + } + /** Caches the position of the drop list. */ private _cacheOwnPosition() { - this._clientRect = coerceElement(this.element).getBoundingClientRect(); + const element = coerceElement(this.element); + this._clientRect = getMutableClientRect(element); + this._scrollPosition = {top: element.scrollTop, left: element.scrollLeft}; } /** Refreshes the position cache of the items and sibling containers. */ @@ -434,24 +584,7 @@ export class DropListRef { // placeholder, because the element is hidden. drag.getPlaceholderElement() : drag.getRootElement(); - const clientRect = elementToMeasure.getBoundingClientRect(); - - return { - drag, - offset: 0, - // We need to clone the `clientRect` here, because all the values on it are readonly - // and we need to be able to update them. Also we can't use a spread here, because - // the values on a `ClientRect` aren't own properties. See: - // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes - clientRect: { - top: clientRect.top, - right: clientRect.right, - bottom: clientRect.bottom, - left: clientRect.left, - width: clientRect.width, - height: clientRect.height - } - }; + return {drag, offset: 0, clientRect: getMutableClientRect(elementToMeasure)}; }).sort((a, b) => { return isHorizontal ? a.clientRect.left - b.clientRect.left : a.clientRect.top - b.clientRect.top; @@ -469,6 +602,8 @@ export class DropListRef { this._itemPositions = []; this._previousSwap.drag = null; this._previousSwap.delta = 0; + this._stopScrolling(); + this._removeListeners(); } /** @@ -581,6 +716,84 @@ export class DropListRef { this._cacheOwnPosition(); } + /** + * Updates the internal state of the container after a scroll event has happened. + * @param scrollPosition Object that is keeping track of the scroll position. + * @param newTop New top scroll position. + * @param newLeft New left scroll position. + * @param extraClientRect Extra `ClientRect` object that should be updated, in addition to the + * ones of the drag items. Useful when the viewport has been scrolled and we also need to update + * the `ClientRect` of the list. + */ + private _updateAfterScroll(scrollPosition: ScrollPosition, newTop: number, newLeft: number, + extraClientRect?: ClientRect) { + const topDifference = scrollPosition.top - newTop; + const leftDifference = scrollPosition.left - newLeft; + + if (extraClientRect) { + adjustClientRect(extraClientRect, topDifference, leftDifference); + } + + // Since we know the amount that the user has scrolled we can shift all of the client rectangles + // ourselves. This is cheaper than re-measuring everything and we can avoid inconsistent + // behavior where we might be measuring the element before its position has changed. + this._itemPositions.forEach(({clientRect}) => { + adjustClientRect(clientRect, topDifference, leftDifference); + }); + + // We need two loops for this, because we want all of the cached + // positions to be up-to-date before we re-sort the item. + this._itemPositions.forEach(({drag}) => { + if (this._dragDropRegistry.isDragging(drag)) { + // We need to re-sort the item manually, because the pointer move + // events won't be dispatched while the user is scrolling. + drag._sortFromLastPointerPosition(); + } + }); + + scrollPosition.top = newTop; + scrollPosition.left = newLeft; + } + + /** Handles the container being scrolled. Has to be an arrow function to preserve the context. */ + private _handleScroll = () => { + if (!this.isDragging()) { + return; + } + + const element = coerceElement(this.element); + this._updateAfterScroll(this._scrollPosition, element.scrollTop, element.scrollLeft); + } + + /** Removes the event listeners associated with this drop list. */ + private _removeListeners() { + coerceElement(this.element).removeEventListener('scroll', this._handleScroll); + this._viewportScrollSubscription.unsubscribe(); + } + + /** Starts the interval that'll auto-scroll the element. */ + private _startScrollInterval = () => { + this._stopScrolling(); + + interval(0, animationFrameScheduler) + .pipe(takeUntil(this._stopScrollTimers)) + .subscribe(() => { + const node = this._scrollNode; + + if (this._verticalScrollDirection === AutoScrollVerticalDirection.UP) { + incrementVerticalScroll(node, -AUTO_SCROLL_STEP); + } else if (this._verticalScrollDirection === AutoScrollVerticalDirection.DOWN) { + incrementVerticalScroll(node, AUTO_SCROLL_STEP); + } + + if (this._horizontalScrollDirection === AutoScrollHorizontalDirection.LEFT) { + incrementHorizontalScroll(node, -AUTO_SCROLL_STEP); + } else if (this._horizontalScrollDirection === AutoScrollHorizontalDirection.RIGHT) { + incrementHorizontalScroll(node, AUTO_SCROLL_STEP); + } + }); + } + /** * Checks whether the user's pointer is positioned over the container. * @param x Pointer position along the X axis. @@ -671,7 +884,7 @@ function adjustClientRect(clientRect: ClientRect, top: number, left: number) { /** * Finds the index of an item that matches a predicate function. Used as an equivalent - * of `Array.prototype.find` which isn't part of the standard Google typings. + * of `Array.prototype.findIndex` which isn't part of the standard Google typings. * @param array Array in which to look for matches. * @param predicate Function used to determine whether an item is a match. */ @@ -698,3 +911,86 @@ function isInsideClientRect(clientRect: ClientRect, x: number, y: number) { const {top, bottom, left, right} = clientRect; return y >= top && y <= bottom && x >= left && x <= right; } + + +/** Gets a mutable version of an element's bounding `ClientRect`. */ +function getMutableClientRect(element: Element): ClientRect { + const clientRect = element.getBoundingClientRect(); + + // We need to clone the `clientRect` here, because all the values on it are readonly + // and we need to be able to update them. Also we can't use a spread here, because + // the values on a `ClientRect` aren't own properties. See: + // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes + return { + top: clientRect.top, + right: clientRect.right, + bottom: clientRect.bottom, + left: clientRect.left, + width: clientRect.width, + height: clientRect.height + }; +} + +/** + * Increments the vertical scroll position of a node. + * @param node Node whose scroll position should change. + * @param amount Amount of pixels that the `node` should be scrolled. + */ +function incrementVerticalScroll(node: HTMLElement | Window, amount: number) { + if (node === window) { + (node as Window).scrollBy(0, amount); + } else { + // Ideally we could use `Element.scrollBy` here as well, but IE and Edge don't support it. + (node as HTMLElement).scrollTop += amount; + } +} + +/** + * Increments the horizontal scroll position of a node. + * @param node Node whose scroll position should change. + * @param amount Amount of pixels that the `node` should be scrolled. + */ +function incrementHorizontalScroll(node: HTMLElement | Window, amount: number) { + if (node === window) { + (node as Window).scrollBy(amount, 0); + } else { + // Ideally we could use `Element.scrollBy` here as well, but IE and Edge don't support it. + (node as HTMLElement).scrollLeft += amount; + } +} + +/** + * Gets whether the vertical auto-scroll direction of a node. + * @param clientRect Dimensions of the node. + * @param pointerY Position of the user's pointer along the y axis. + */ +function getVerticalScrollDirection(clientRect: ClientRect, pointerY: number) { + const {top, bottom, height} = clientRect; + const yThreshold = height * SCROLL_PROXIMITY_THRESHOLD; + + if (pointerY >= top - yThreshold && pointerY <= top + yThreshold) { + return AutoScrollVerticalDirection.UP; + } else if (pointerY >= bottom - yThreshold && pointerY <= bottom + yThreshold) { + return AutoScrollVerticalDirection.DOWN; + } + + return AutoScrollVerticalDirection.NONE; +} + +/** + * Gets whether the horizontal auto-scroll direction of a node. + * @param clientRect Dimensions of the node. + * @param pointerX Position of the user's pointer along the x axis. + */ +function getHorizontalScrollDirection(clientRect: ClientRect, pointerX: number) { + const {left, right, width} = clientRect; + const xThreshold = width * SCROLL_PROXIMITY_THRESHOLD; + + if (pointerX >= left - xThreshold && pointerX <= left + xThreshold) { + return AutoScrollHorizontalDirection.LEFT; + } else if (pointerX >= right - xThreshold && pointerX <= right + xThreshold) { + return AutoScrollHorizontalDirection.RIGHT; + } + + return AutoScrollHorizontalDirection.NONE; +} diff --git a/tools/public_api_guard/cdk/drag-drop.d.ts b/tools/public_api_guard/cdk/drag-drop.d.ts index 7706874fc96c..0a421d9e4611 100644 --- a/tools/public_api_guard/cdk/drag-drop.d.ts +++ b/tools/public_api_guard/cdk/drag-drop.d.ts @@ -138,6 +138,7 @@ export interface CdkDragStart { export declare class CdkDropList implements CdkDropListContainer, AfterContentInit, OnDestroy { _draggables: QueryList; _dropListRef: DropListRef>; + autoScrollDisabled: boolean; connectedTo: (CdkDropList | string)[] | CdkDropList | string; data: T; disabled: boolean; @@ -211,6 +212,7 @@ export declare class DragDropRegistry implements OnDestroy { readonly pointerMove: Subject; readonly pointerUp: Subject; + readonly scroll: Subject; constructor(_ngZone: NgZone, _document: any); getDropContainer(id: string): C | undefined; isDragging(drag: I): boolean; @@ -272,6 +274,7 @@ export declare class DragRef { source: DragRef; }>; constructor(element: ElementRef | HTMLElement, _config: DragRefConfig, _document: Document, _ngZone: NgZone, _viewportRuler: ViewportRuler, _dragDropRegistry: DragDropRegistry); + _sortFromLastPointerPosition(): void; _withDropContainer(container: DropListRef): void; disableHandle(handle: HTMLElement): void; dispose(): void; @@ -296,6 +299,7 @@ export interface DragRefConfig { } export declare class DropListRef { + autoScrollDisabled: boolean; beforeStarted: Subject; data: T; disabled: boolean; @@ -328,7 +332,8 @@ export declare class DropListRef { item: DragRef; }>; sortingDisabled: boolean; - constructor(element: ElementRef | HTMLElement, _dragDropRegistry: DragDropRegistry, _document: any); + constructor(element: ElementRef | HTMLElement, _dragDropRegistry: DragDropRegistry, _document: any, + _ngZone?: NgZone | undefined, _viewportRuler?: ViewportRuler | undefined); _canReceive(item: DragRef, x: number, y: number): boolean; _getSiblingContainerFromPosition(item: DragRef, x: number, y: number): DropListRef | undefined; _isOverContainer(x: number, y: number): boolean; @@ -337,7 +342,9 @@ export declare class DropListRef { y: number; }): void; _startReceiving(sibling: DropListRef): void; + _startScrollingIfNecessary(pointerX: number, pointerY: number): void; _stopReceiving(sibling: DropListRef): void; + _stopScrolling(): void; connectedTo(connectedTo: DropListRef[]): this; dispose(): void; drop(item: DragRef, currentIndex: number, previousContainer: DropListRef, isPointerOverContainer: boolean, distance?: Point): void;