Skip to content

Commit 076e451

Browse files
committed
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.
1 parent aad7ff7 commit 076e451

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,24 @@ describe('CdkDrag', () => {
763763
expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)');
764764
}));
765765

766+
it('should adjust the x offset if the boundary becomes narrower after a viewport resize',
767+
fakeAsync(() => {
768+
const fixture = createComponent(StandaloneDraggable);
769+
const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper');
770+
fixture.componentInstance.boundary = boundary;
771+
fixture.detectChanges();
772+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
773+
774+
dragElementViaMouse(fixture, dragElement, 300, 300);
775+
expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)');
776+
777+
boundary.style.width = '150px';
778+
dispatchFakeEvent(window, 'resize');
779+
tick(20);
780+
781+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
782+
}));
783+
766784
it('should handle the element and boundary dimensions changing between drag sequences',
767785
fakeAsync(() => {
768786
const fixture = createComponent(StandaloneDraggable);
@@ -782,6 +800,60 @@ describe('CdkDrag', () => {
782800
expect(dragElement.style.transform).toBe('translate3d(150px, 150px, 0px)');
783801
}));
784802

803+
it('should adjust the y offset if the boundary becomes shorter after a viewport resize',
804+
fakeAsync(() => {
805+
const fixture = createComponent(StandaloneDraggable);
806+
const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper');
807+
fixture.componentInstance.boundary = boundary;
808+
fixture.detectChanges();
809+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
810+
811+
dragElementViaMouse(fixture, dragElement, 300, 300);
812+
expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)');
813+
814+
boundary.style.height = '150px';
815+
dispatchFakeEvent(window, 'resize');
816+
tick(20);
817+
818+
expect(dragElement.style.transform).toBe('translate3d(100px, 50px, 0px)');
819+
}));
820+
821+
it('should reset the x offset if the boundary becomes narrower than the element ' +
822+
'after a resize', fakeAsync(() => {
823+
const fixture = createComponent(StandaloneDraggable);
824+
const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper');
825+
fixture.componentInstance.boundary = boundary;
826+
fixture.detectChanges();
827+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
828+
829+
dragElementViaMouse(fixture, dragElement, 300, 300);
830+
expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)');
831+
832+
boundary.style.width = '50px';
833+
dispatchFakeEvent(window, 'resize');
834+
tick(20);
835+
836+
expect(dragElement.style.transform).toBe('translate3d(0px, 100px, 0px)');
837+
}));
838+
839+
it('should reset the y offset if the boundary becomes shorter than the element after a resize',
840+
fakeAsync(() => {
841+
const fixture = createComponent(StandaloneDraggable);
842+
const boundary: HTMLElement = fixture.nativeElement.querySelector('.wrapper');
843+
fixture.componentInstance.boundary = boundary;
844+
fixture.detectChanges();
845+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
846+
847+
dragElementViaMouse(fixture, dragElement, 300, 300);
848+
expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)');
849+
850+
boundary.style.height = '50px';
851+
dispatchFakeEvent(window, 'resize');
852+
tick(20);
853+
854+
expect(dragElement.style.transform).toBe('translate3d(100px, 0px, 0px)');
855+
}));
856+
785857
it('should allow for the position constrain logic to be customized', fakeAsync(() => {
786858
const fixture = createComponent(StandaloneDraggable);
787859
const spy = jasmine.createSpy('constrain position spy').and.returnValue({

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ export class DragRef<T = any> {
158158
/** Subscription to the viewport being scrolled. */
159159
private _scrollSubscription = Subscription.EMPTY;
160160

161+
/** Subscription to the viewport being resized. */
162+
private _resizeSubscription = Subscription.EMPTY;
163+
161164
/**
162165
* Time at which the last touch event occurred. Used to avoid firing the same
163166
* events multiple times on touch devices where the browser will fire a fake
@@ -351,6 +354,12 @@ export class DragRef<T = any> {
351354
*/
352355
withBoundaryElement(boundaryElement: ElementRef<HTMLElement> | HTMLElement | null): this {
353356
this._boundaryElement = boundaryElement ? coerceElement(boundaryElement) : null;
357+
this._resizeSubscription.unsubscribe();
358+
if (boundaryElement) {
359+
this._resizeSubscription = this._viewportRuler
360+
.change(10)
361+
.subscribe(() => this._containInsideBoundaryOnResize());
362+
}
354363
return this;
355364
}
356365

@@ -1086,6 +1095,57 @@ export class DragRef<T = any> {
10861095
private _cleanupCachedDimensions() {
10871096
this._boundaryRect = this._previewRect = undefined;
10881097
}
1098+
1099+
/**
1100+
* Checks whether the element is still inside its boundary after the viewport has been resized.
1101+
* If not, the position is adjusted so that the element fits again.
1102+
*/
1103+
private _containInsideBoundaryOnResize() {
1104+
let {x, y} = this._passiveTransform;
1105+
1106+
if ((x === 0 && y === 0) || this.isDragging() || !this._boundaryElement) {
1107+
return;
1108+
}
1109+
1110+
const boundaryRect = this._boundaryElement.getBoundingClientRect();
1111+
const elementRect = this._rootElement.getBoundingClientRect();
1112+
const leftOverflow = boundaryRect.left - elementRect.left;
1113+
const rightOverflow = elementRect.right - boundaryRect.right;
1114+
const topOverflow = boundaryRect.top - elementRect.top;
1115+
const bottomOverflow = elementRect.bottom - boundaryRect.bottom;
1116+
1117+
// If the element has become wider than the boundary, we can't
1118+
// do much to make it fit so we just anchor it to the left.
1119+
if (boundaryRect.width > elementRect.width) {
1120+
if (leftOverflow > 0) {
1121+
x += leftOverflow;
1122+
}
1123+
1124+
if (rightOverflow > 0) {
1125+
x -= rightOverflow;
1126+
}
1127+
} else {
1128+
x = 0;
1129+
}
1130+
1131+
// If the element has become taller than the boundary, we can't
1132+
// do much to make it fit so we just anchor it to the top.
1133+
if (boundaryRect.height > elementRect.height) {
1134+
if (topOverflow > 0) {
1135+
y += topOverflow;
1136+
}
1137+
1138+
if (bottomOverflow > 0) {
1139+
y -= bottomOverflow;
1140+
}
1141+
} else {
1142+
y = 0;
1143+
}
1144+
1145+
if (x !== this._passiveTransform.x || y !== this._passiveTransform.y) {
1146+
this.setFreeDragPosition({y, x});
1147+
}
1148+
}
10891149
}
10901150

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

0 commit comments

Comments
 (0)