Skip to content

Commit cf61960

Browse files
authored
feat(cdk/drag-drop): introduce resetToBoundary (#30436)
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
1 parent 46344d8 commit cf61960

File tree

5 files changed

+181
-1
lines changed

5 files changed

+181
-1
lines changed

goldens/cdk/drag-drop/index.api.md

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
8888
_resetPlaceholderTemplate(placeholder: CdkDragPlaceholder): void;
8989
// (undocumented)
9090
_resetPreviewTemplate(preview: CdkDragPreview): void;
91+
resetToBoundary(): void;
9192
rootElementSelector: string;
9293
scale: number;
9394
setFreeDragPosition(value: Point): void;
@@ -439,6 +440,7 @@ export class DragRef<T = any> {
439440
event: MouseEvent | TouchEvent;
440441
}>;
441442
reset(): void;
443+
resetToBoundary(): void;
442444
scale: number;
443445
setFreeDragPosition(value: Point): this;
444446
_sortFromLastPointerPosition(): void;

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

+5
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
279279
this._dragRef.reset();
280280
}
281281

282+
/** Resets drag item to end of boundary element. */
283+
resetToBoundary() {
284+
this._dragRef.resetToBoundary();
285+
}
286+
282287
/**
283288
* Gets the pixel coordinates of the draggable outside of a drop container.
284289
*/

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

+112
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
startDraggingViaMouse,
3434
startDraggingViaTouch,
3535
} from './test-utils.spec';
36+
import {isInsideClientRect, isOverflowingParent} from '../dom/dom-rect';
3637

3738
describe('Standalone CdkDrag', () => {
3839
describe('mouse dragging', () => {
@@ -46,6 +47,95 @@ describe('Standalone CdkDrag', () => {
4647
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
4748
}));
4849

50+
it('should reset drag item to boundary', fakeAsync(() => {
51+
const fixture = createComponent(DragWithResizeableBoundary);
52+
fixture.detectChanges();
53+
let boundaryElement = fixture.componentInstance.boundaryElement.nativeElement;
54+
let dragElement = fixture.componentInstance.dragElement.nativeElement;
55+
56+
dragElementViaMouse(fixture, dragElement, 50, 100);
57+
58+
// check if the drag element is within the boundary or not
59+
expect(
60+
isInsideClientRect(
61+
boundaryElement.getBoundingClientRect(),
62+
fixture.componentInstance.dragInstance.getFreeDragPosition().x,
63+
fixture.componentInstance.dragInstance.getFreeDragPosition().y,
64+
),
65+
).toBeTrue();
66+
67+
// drag it till the end of the boundary
68+
dragElementViaMouse(fixture, dragElement, 400, 400);
69+
70+
// it should still be present within the boundary
71+
expect(
72+
isInsideClientRect(
73+
boundaryElement.getBoundingClientRect(),
74+
fixture.componentInstance.dragInstance.getFreeDragPosition().x,
75+
fixture.componentInstance.dragInstance.getFreeDragPosition().y,
76+
),
77+
).toBeTrue();
78+
79+
// shrink boundary to check if we are within boundary or not
80+
fixture.componentInstance.setBoundary('200px', '200px');
81+
fixture.detectChanges();
82+
83+
// it should not be within the boundary anymore as we shrinked it
84+
expect(
85+
isInsideClientRect(
86+
boundaryElement.getBoundingClientRect(),
87+
fixture.componentInstance.dragInstance.getFreeDragPosition().x,
88+
fixture.componentInstance.dragInstance.getFreeDragPosition().y,
89+
),
90+
).toBeFalse();
91+
92+
fixture.componentInstance.dragInstance.resetToBoundary();
93+
fixture.detectChanges();
94+
95+
// should be be within bounding box of its boundary now that we have reseted it
96+
expect(
97+
isInsideClientRect(
98+
boundaryElement.getBoundingClientRect(),
99+
fixture.componentInstance.dragInstance.getFreeDragPosition().x,
100+
fixture.componentInstance.dragInstance.getFreeDragPosition().y,
101+
),
102+
).toBeTrue();
103+
104+
// expand the boundary enough that so can we can make the draggable item to be overflown
105+
// of its parent from top side
106+
fixture.componentInstance.setBoundary('500px', '500px');
107+
fixture.detectChanges();
108+
109+
// drag it till the end of the boundary
110+
dragElementViaMouse(fixture, dragElement, 500, 500);
111+
112+
// shrink boundary to make draggable item to be overflown
113+
fixture.componentInstance.setBoundary('400px', '400px');
114+
fixture.detectChanges();
115+
116+
// should be within bounding rect but it's overflowing as it was placed in a way that
117+
// it is overflowing
118+
expect(
119+
isOverflowingParent(
120+
boundaryElement.getBoundingClientRect(),
121+
dragElement.getBoundingClientRect(),
122+
),
123+
).toBeTrue();
124+
125+
// reset it so that overflowing offset is fixed
126+
fixture.componentInstance.dragInstance.resetToBoundary();
127+
fixture.detectChanges();
128+
129+
// should be within bounding rect but it's overflowing as it was placed in a way that
130+
// it is overflowing
131+
expect(
132+
isOverflowingParent(
133+
boundaryElement.getBoundingClientRect(),
134+
dragElement.getBoundingClientRect(),
135+
),
136+
).toBeFalse();
137+
}));
138+
49139
it('should drag an element freely to a particular position when the page is scrolled', fakeAsync(() => {
50140
const fixture = createComponent(StandaloneDraggable);
51141
fixture.detectChanges();
@@ -2047,3 +2137,25 @@ class PlainStandaloneDraggable {
20472137
class StandaloneDraggableWithExternalTemplateHandle {
20482138
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
20492139
}
2140+
2141+
@Component({
2142+
template: `
2143+
<div #boundaryElement class="example-boundary" style="width: 400px; height: 400px">
2144+
<div #dragElement class="example-box" cdkDragBoundary=".example-boundary" cdkDrag style="width: 100px; height: 100px">
2145+
I can only be dragged within the container
2146+
</div>
2147+
</div>
2148+
`,
2149+
imports: [CdkDrag],
2150+
})
2151+
class DragWithResizeableBoundary {
2152+
@ViewChild('boundaryElement') boundaryElement: ElementRef<HTMLElement>;
2153+
2154+
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
2155+
@ViewChild(CdkDrag) dragInstance: CdkDrag;
2156+
2157+
setBoundary(height: string, width: string) {
2158+
this.boundaryElement.nativeElement.style.height = height;
2159+
this.boundaryElement.nativeElement.style.width = width;
2160+
}
2161+
}

src/cdk/drag-drop/dom/dom-rect.ts

+17
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@ export function isInsideClientRect(clientRect: DOMRect, x: number, y: number) {
3737
return y >= top && y <= bottom && x >= left && x <= right;
3838
}
3939

40+
/**
41+
* Checks if the child element is overflowing from its parent.
42+
* @param parentRect - The bounding rect of the parent element.
43+
* @param childRect - The bounding rect of the child element.
44+
*/
45+
export function isOverflowingParent(parentRect: DOMRect, childRect: DOMRect): boolean {
46+
// check for horizontal overflow (left and right)
47+
const isLeftOverflowing = childRect.left < parentRect.left;
48+
const isRightOverflowing = childRect.left + childRect.width > parentRect.right;
49+
50+
// check for vertical overflow (top and bottom)
51+
const isTopOverflowing = childRect.top < parentRect.top;
52+
const isBottomOverflowing = childRect.top + childRect.height > parentRect.bottom;
53+
54+
return isLeftOverflowing || isRightOverflowing || isTopOverflowing || isBottomOverflowing;
55+
}
56+
4057
/**
4158
* Updates the top/left positions of a `DOMRect`, as well as their bottom/right counterparts.
4259
* @param domRect `DOMRect` that should be updated.

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

+45-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
} from '@angular/core';
2323
import {Observable, Subject, Subscription} from 'rxjs';
2424
import {deepCloneNode} from './dom/clone-node';
25-
import {adjustDomRect, getMutableClientRect} from './dom/dom-rect';
25+
import {adjustDomRect, getMutableClientRect, isOverflowingParent} from './dom/dom-rect';
2626
import {ParentPositionTracker} from './dom/parent-position-tracker';
2727
import {getRootNode} from './dom/root-node';
2828
import {
@@ -546,6 +546,50 @@ export class DragRef<T = any> {
546546
this._passiveTransform = {x: 0, y: 0};
547547
}
548548

549+
/** Resets drag item to end of boundary element. */
550+
resetToBoundary(): void {
551+
if (
552+
// can be null if the drag item was never dragged.
553+
this._boundaryElement &&
554+
this._rootElement &&
555+
// check if we are overflowing off our boundary element
556+
isOverflowingParent(
557+
this._boundaryElement.getBoundingClientRect(),
558+
this._rootElement.getBoundingClientRect(),
559+
)
560+
) {
561+
const parentRect = this._boundaryElement.getBoundingClientRect();
562+
const childRect = this._rootElement.getBoundingClientRect();
563+
564+
let offsetX = 0;
565+
let offsetY = 0;
566+
567+
// check if we are overflowing from left or right
568+
if (childRect.left < parentRect.left) {
569+
offsetX = parentRect.left - childRect.left;
570+
} else if (childRect.right > parentRect.right) {
571+
offsetX = parentRect.right - childRect.right;
572+
}
573+
574+
// check if we are overflowing from top or bottom
575+
if (childRect.top < parentRect.top) {
576+
offsetY = parentRect.top - childRect.top;
577+
} else if (childRect.bottom > parentRect.bottom) {
578+
offsetY = parentRect.bottom - childRect.bottom;
579+
}
580+
581+
const currentLeft = this._activeTransform.x;
582+
const currentTop = this._activeTransform.y;
583+
584+
let x = currentLeft + offsetX,
585+
y = currentTop + offsetY;
586+
587+
this._rootElement.style.transform = getTransform(x, y);
588+
this._activeTransform = {x, y};
589+
this._passiveTransform = {x, y};
590+
}
591+
}
592+
549593
/**
550594
* Sets a handle as disabled. While a handle is disabled, it'll capture and interrupt dragging.
551595
* @param handle Handle element that should be disabled.

0 commit comments

Comments
 (0)