From 294eafa2cfd54fafc7920d84207af6950a27bdfd Mon Sep 17 00:00:00 2001 From: naaajii Date: Mon, 3 Feb 2025 00:15:28 +0500 Subject: [PATCH] feat(cdk/drag-drop): introduce `resetToBoundary` this commit introduces `resetToBoundary` in DragRef which allows user to align DragItem to its boundary on demand if at one point it was at a place where the boundary element used to be and has shrinked causing DragItem to be outside of the boundary box fixes #30325 --- goldens/cdk/drag-drop/index.api.md | 2 + src/cdk/drag-drop/directives/drag.ts | 5 + .../directives/standalone-drag.spec.ts | 112 ++++++++++++++++++ src/cdk/drag-drop/dom/dom-rect.ts | 17 +++ src/cdk/drag-drop/drag-ref.ts | 46 ++++++- 5 files changed, 181 insertions(+), 1 deletion(-) diff --git a/goldens/cdk/drag-drop/index.api.md b/goldens/cdk/drag-drop/index.api.md index 55253d56dbc7..23201eb6c083 100644 --- a/goldens/cdk/drag-drop/index.api.md +++ b/goldens/cdk/drag-drop/index.api.md @@ -88,6 +88,7 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { _resetPlaceholderTemplate(placeholder: CdkDragPlaceholder): void; // (undocumented) _resetPreviewTemplate(preview: CdkDragPreview): void; + resetToBoundary(): void; rootElementSelector: string; scale: number; setFreeDragPosition(value: Point): void; @@ -439,6 +440,7 @@ export class DragRef { event: MouseEvent | TouchEvent; }>; reset(): void; + resetToBoundary(): void; scale: number; setFreeDragPosition(value: Point): this; _sortFromLastPointerPosition(): void; diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index b30c42a2fe5e..9ac4ef7f1261 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -279,6 +279,11 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { this._dragRef.reset(); } + /** Resets drag item to end of boundary element. */ + resetToBoundary() { + this._dragRef.resetToBoundary(); + } + /** * Gets the pixel coordinates of the draggable outside of a drop container. */ diff --git a/src/cdk/drag-drop/directives/standalone-drag.spec.ts b/src/cdk/drag-drop/directives/standalone-drag.spec.ts index a40a5e76d345..e8cf6bdcbd2b 100644 --- a/src/cdk/drag-drop/directives/standalone-drag.spec.ts +++ b/src/cdk/drag-drop/directives/standalone-drag.spec.ts @@ -33,6 +33,7 @@ import { startDraggingViaMouse, startDraggingViaTouch, } from './test-utils.spec'; +import {isInsideClientRect, isOverflowingParent} from '../dom/dom-rect'; describe('Standalone CdkDrag', () => { describe('mouse dragging', () => { @@ -46,6 +47,95 @@ describe('Standalone CdkDrag', () => { expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); })); + it('should reset drag item to boundary', fakeAsync(() => { + const fixture = createComponent(DragWithResizeableBoundary); + fixture.detectChanges(); + let boundaryElement = fixture.componentInstance.boundaryElement.nativeElement; + let dragElement = fixture.componentInstance.dragElement.nativeElement; + + dragElementViaMouse(fixture, dragElement, 50, 100); + + // check if the drag element is within the boundary or not + expect( + isInsideClientRect( + boundaryElement.getBoundingClientRect(), + fixture.componentInstance.dragInstance.getFreeDragPosition().x, + fixture.componentInstance.dragInstance.getFreeDragPosition().y, + ), + ).toBeTrue(); + + // drag it till the end of the boundary + dragElementViaMouse(fixture, dragElement, 400, 400); + + // it should still be present within the boundary + expect( + isInsideClientRect( + boundaryElement.getBoundingClientRect(), + fixture.componentInstance.dragInstance.getFreeDragPosition().x, + fixture.componentInstance.dragInstance.getFreeDragPosition().y, + ), + ).toBeTrue(); + + // shrink boundary to check if we are within boundary or not + fixture.componentInstance.setBoundary('200px', '200px'); + fixture.detectChanges(); + + // it should not be within the boundary anymore as we shrinked it + expect( + isInsideClientRect( + boundaryElement.getBoundingClientRect(), + fixture.componentInstance.dragInstance.getFreeDragPosition().x, + fixture.componentInstance.dragInstance.getFreeDragPosition().y, + ), + ).toBeFalse(); + + fixture.componentInstance.dragInstance.resetToBoundary(); + fixture.detectChanges(); + + // should be be within bounding box of its boundary now that we have reseted it + expect( + isInsideClientRect( + boundaryElement.getBoundingClientRect(), + fixture.componentInstance.dragInstance.getFreeDragPosition().x, + fixture.componentInstance.dragInstance.getFreeDragPosition().y, + ), + ).toBeTrue(); + + // expand the boundary enough that so can we can make the draggable item to be overflown + // of its parent from top side + fixture.componentInstance.setBoundary('500px', '500px'); + fixture.detectChanges(); + + // drag it till the end of the boundary + dragElementViaMouse(fixture, dragElement, 500, 500); + + // shrink boundary to make draggable item to be overflown + fixture.componentInstance.setBoundary('400px', '400px'); + fixture.detectChanges(); + + // should be within bounding rect but it's overflowing as it was placed in a way that + // it is overflowing + expect( + isOverflowingParent( + boundaryElement.getBoundingClientRect(), + dragElement.getBoundingClientRect(), + ), + ).toBeTrue(); + + // reset it so that overflowing offset is fixed + fixture.componentInstance.dragInstance.resetToBoundary(); + fixture.detectChanges(); + + // should be within bounding rect but it's overflowing as it was placed in a way that + // it is overflowing + expect( + isOverflowingParent( + boundaryElement.getBoundingClientRect(), + dragElement.getBoundingClientRect(), + ), + ).toBeFalse(); + })); + it('should drag an element freely to a particular position when the page is scrolled', fakeAsync(() => { const fixture = createComponent(StandaloneDraggable); fixture.detectChanges(); @@ -2047,3 +2137,25 @@ class PlainStandaloneDraggable { class StandaloneDraggableWithExternalTemplateHandle { @ViewChild('dragElement') dragElement: ElementRef; } + +@Component({ + template: ` +
+
+ I can only be dragged within the container +
+
+ `, + imports: [CdkDrag], +}) +class DragWithResizeableBoundary { + @ViewChild('boundaryElement') boundaryElement: ElementRef; + + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChild(CdkDrag) dragInstance: CdkDrag; + + setBoundary(height: string, width: string) { + this.boundaryElement.nativeElement.style.height = height; + this.boundaryElement.nativeElement.style.width = width; + } +} diff --git a/src/cdk/drag-drop/dom/dom-rect.ts b/src/cdk/drag-drop/dom/dom-rect.ts index ae19cd77d1ab..6ddfa6695485 100644 --- a/src/cdk/drag-drop/dom/dom-rect.ts +++ b/src/cdk/drag-drop/dom/dom-rect.ts @@ -37,6 +37,23 @@ export function isInsideClientRect(clientRect: DOMRect, x: number, y: number) { return y >= top && y <= bottom && x >= left && x <= right; } +/** + * Checks if the child element is overflowing from its parent. + * @param parentRect - The bounding rect of the parent element. + * @param childRect - The bounding rect of the child element. + */ +export function isOverflowingParent(parentRect: DOMRect, childRect: DOMRect): boolean { + // check for horizontal overflow (left and right) + const isLeftOverflowing = childRect.left < parentRect.left; + const isRightOverflowing = childRect.left + childRect.width > parentRect.right; + + // check for vertical overflow (top and bottom) + const isTopOverflowing = childRect.top < parentRect.top; + const isBottomOverflowing = childRect.top + childRect.height > parentRect.bottom; + + return isLeftOverflowing || isRightOverflowing || isTopOverflowing || isBottomOverflowing; +} + /** * Updates the top/left positions of a `DOMRect`, as well as their bottom/right counterparts. * @param domRect `DOMRect` that should be updated. diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index 45dea4eac3dd..175e52348d27 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -22,7 +22,7 @@ import { } from '@angular/core'; import {Observable, Subject, Subscription} from 'rxjs'; import {deepCloneNode} from './dom/clone-node'; -import {adjustDomRect, getMutableClientRect} from './dom/dom-rect'; +import {adjustDomRect, getMutableClientRect, isOverflowingParent} from './dom/dom-rect'; import {ParentPositionTracker} from './dom/parent-position-tracker'; import {getRootNode} from './dom/root-node'; import { @@ -546,6 +546,50 @@ export class DragRef { this._passiveTransform = {x: 0, y: 0}; } + /** Resets drag item to end of boundary element. */ + resetToBoundary(): void { + if ( + // can be null if the drag item was never dragged. + this._boundaryElement && + this._rootElement && + // check if we are overflowing off our boundary element + isOverflowingParent( + this._boundaryElement.getBoundingClientRect(), + this._rootElement.getBoundingClientRect(), + ) + ) { + const parentRect = this._boundaryElement.getBoundingClientRect(); + const childRect = this._rootElement.getBoundingClientRect(); + + let offsetX = 0; + let offsetY = 0; + + // check if we are overflowing from left or right + if (childRect.left < parentRect.left) { + offsetX = parentRect.left - childRect.left; + } else if (childRect.right > parentRect.right) { + offsetX = parentRect.right - childRect.right; + } + + // check if we are overflowing from top or bottom + if (childRect.top < parentRect.top) { + offsetY = parentRect.top - childRect.top; + } else if (childRect.bottom > parentRect.bottom) { + offsetY = parentRect.bottom - childRect.bottom; + } + + const currentLeft = this._activeTransform.x; + const currentTop = this._activeTransform.y; + + let x = currentLeft + offsetX, + y = currentTop + offsetY; + + this._rootElement.style.transform = getTransform(x, y); + this._activeTransform = {x, y}; + this._passiveTransform = {x, y}; + } + } + /** * Sets a handle as disabled. While a handle is disabled, it'll capture and interrupt dragging. * @param handle Handle element that should be disabled.