From 076e451a1f65ffaf565e23b09b3a04f46cfdde27 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sat, 31 Aug 2019 10:04:31 +0200 Subject: [PATCH] fix(drag-drop): handle the element going out of the boundary after a viewport resize Adds some logic to ensure that an element inside a boundary is contained within it after the viewport is resized and the boundary may have changed size. Fixes #16536. --- src/cdk/drag-drop/directives/drag.spec.ts | 72 +++++++++++++++++++++++ src/cdk/drag-drop/drag-ref.ts | 60 +++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index f91c484dbd7a..c254134eef38 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -763,6 +763,24 @@ describe('CdkDrag', () => { expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); })); + it('should adjust the x offset if the boundary becomes narrower after a viewport resize', + fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.width = '150px'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); + it('should handle the element and boundary dimensions changing between drag sequences', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); @@ -782,6 +800,60 @@ describe('CdkDrag', () => { expect(dragElement.style.transform).toBe('translate3d(150px, 150px, 0px)'); })); + it('should adjust the y offset if the boundary becomes shorter after a viewport resize', + fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.height = '150px'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(100px, 50px, 0px)'); + })); + + it('should reset the x offset if the boundary becomes narrower than the element ' + + 'after a resize', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.width = '50px'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(0px, 100px, 0px)'); + })); + + it('should reset the y offset if the boundary becomes shorter than the element after a resize', + fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper'); + fixture.componentInstance.boundary = boundary; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 300, 300); + expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); + + boundary.style.height = '50px'; + dispatchFakeEvent(window, 'resize'); + tick(20); + + expect(dragElement.style.transform).toBe('translate3d(100px, 0px, 0px)'); + })); + it('should allow for the position constrain logic to be customized', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); const spy = jasmine.createSpy('constrain position spy').and.returnValue({ diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index 8b3b09e2729d..a04f9e030a7d 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -158,6 +158,9 @@ export class DragRef { /** Subscription to the viewport being scrolled. */ private _scrollSubscription = Subscription.EMPTY; + /** Subscription to the viewport being resized. */ + private _resizeSubscription = 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 @@ -351,6 +354,12 @@ export class DragRef { */ withBoundaryElement(boundaryElement: ElementRef | HTMLElement | null): this { this._boundaryElement = boundaryElement ? coerceElement(boundaryElement) : null; + this._resizeSubscription.unsubscribe(); + if (boundaryElement) { + this._resizeSubscription = this._viewportRuler + .change(10) + .subscribe(() => this._containInsideBoundaryOnResize()); + } return this; } @@ -1086,6 +1095,57 @@ export class DragRef { private _cleanupCachedDimensions() { this._boundaryRect = this._previewRect = undefined; } + + /** + * Checks whether the element is still inside its boundary after the viewport has been resized. + * If not, the position is adjusted so that the element fits again. + */ + private _containInsideBoundaryOnResize() { + let {x, y} = this._passiveTransform; + + if ((x === 0 && y === 0) || this.isDragging() || !this._boundaryElement) { + return; + } + + const boundaryRect = this._boundaryElement.getBoundingClientRect(); + const elementRect = this._rootElement.getBoundingClientRect(); + const leftOverflow = boundaryRect.left - elementRect.left; + const rightOverflow = elementRect.right - boundaryRect.right; + const topOverflow = boundaryRect.top - elementRect.top; + const bottomOverflow = elementRect.bottom - boundaryRect.bottom; + + // If the element has become wider than the boundary, we can't + // do much to make it fit so we just anchor it to the left. + if (boundaryRect.width > elementRect.width) { + if (leftOverflow > 0) { + x += leftOverflow; + } + + if (rightOverflow > 0) { + x -= rightOverflow; + } + } else { + x = 0; + } + + // If the element has become taller than the boundary, we can't + // do much to make it fit so we just anchor it to the top. + if (boundaryRect.height > elementRect.height) { + if (topOverflow > 0) { + y += topOverflow; + } + + if (bottomOverflow > 0) { + y -= bottomOverflow; + } + } else { + y = 0; + } + + if (x !== this._passiveTransform.x || y !== this._passiveTransform.y) { + this.setFreeDragPosition({y, x}); + } + } } /** Point on the page or within an element. */