Skip to content

Commit c83feb5

Browse files
committed
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
1 parent 3d7f271 commit c83feb5

File tree

5 files changed

+181
-1
lines changed

5 files changed

+181
-1
lines changed

Diff for: src/cdk/drag-drop/directives/drag.ts

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

287+
/** Resets drag item to end of boundary element. */
288+
resetToBoundary() {
289+
this._dragRef.resetToBoundary();
290+
}
291+
287292
/**
288293
* Gets the pixel coordinates of the draggable outside of a drop container.
289294
*/

Diff for: 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+
}

Diff for: 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.

Diff for: 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 {
@@ -560,6 +560,50 @@ export class DragRef<T = any> {
560560
this._passiveTransform = {x: 0, y: 0};
561561
}
562562

563+
/** Resets drag item to end of boundary element. */
564+
resetToBoundary(): void {
565+
if (
566+
// can be null if the drag item was never dragged.
567+
this._boundaryElement &&
568+
this._rootElement &&
569+
// check if we are overflowing off our boundary element
570+
isOverflowingParent(
571+
this._boundaryElement.getBoundingClientRect(),
572+
this._rootElement.getBoundingClientRect(),
573+
)
574+
) {
575+
const parentRect = this._boundaryElement.getBoundingClientRect();
576+
const childRect = this._rootElement.getBoundingClientRect();
577+
578+
let offsetX = 0;
579+
let offsetY = 0;
580+
581+
// check if we are overflowing from left or right
582+
if (childRect.left < parentRect.left) {
583+
offsetX = parentRect.left - childRect.left;
584+
} else if (childRect.right > parentRect.right) {
585+
offsetX = parentRect.right - childRect.right;
586+
}
587+
588+
// check if we are overflowing from top or bottom
589+
if (childRect.top < parentRect.top) {
590+
offsetY = parentRect.top - childRect.top;
591+
} else if (childRect.bottom > parentRect.bottom) {
592+
offsetY = parentRect.bottom - childRect.bottom;
593+
}
594+
595+
const currentLeft = this._activeTransform.x;
596+
const currentTop = this._activeTransform.y;
597+
598+
let x = currentLeft + offsetX,
599+
y = currentTop + offsetY;
600+
601+
this._rootElement.style.transform = getTransform(x, y);
602+
this._activeTransform = {x, y};
603+
this._passiveTransform = {x, y};
604+
}
605+
}
606+
563607
/**
564608
* Sets a handle as disabled. While a handle is disabled, it'll capture and interrupt dragging.
565609
* @param handle Handle element that should be disabled.

Diff for: tools/public_api_guard/cdk/drag-drop.md

+2
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
9090
_resetPlaceholderTemplate(placeholder: CdkDragPlaceholder): void;
9191
// (undocumented)
9292
_resetPreviewTemplate(preview: CdkDragPreview): void;
93+
resetToBoundary(): void;
9394
rootElementSelector: string;
9495
scale: number;
9596
setFreeDragPosition(value: Point): void;
@@ -443,6 +444,7 @@ export class DragRef<T = any> {
443444
event: MouseEvent | TouchEvent;
444445
}>;
445446
reset(): void;
447+
resetToBoundary(): void;
446448
scale: number;
447449
setFreeDragPosition(value: Point): this;
448450
_sortFromLastPointerPosition(): void;

0 commit comments

Comments
 (0)