Skip to content

Commit 41d5bb7

Browse files
committed
feat(drag-drop): add option to match size of dragged element in custom preview
By default we don't resize custom previews, because we'd have to make assumptions about what the consumer wants to show. These changes add the `matchSize` input which allows the consumer to opt into matching the custom preview size to the dragged element size. Fixes #18177.
1 parent 12edc0b commit 41d5bb7

File tree

5 files changed

+87
-19
lines changed

5 files changed

+87
-19
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {Directive, TemplateRef, Input} from '@angular/core';
10+
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
1011

1112
/**
1213
* Element that will be used as a template for the preview
@@ -18,5 +19,14 @@ import {Directive, TemplateRef, Input} from '@angular/core';
1819
export class CdkDragPreview<T = any> {
1920
/** Context data to be added to the preview template instance. */
2021
@Input() data: T;
22+
23+
/** Whether the preview should preserve the same size as the item that is being dragged. */
24+
@Input()
25+
get matchSize(): boolean { return this._matchSize; }
26+
set matchSize(value: boolean) { this._matchSize = coerceBooleanProperty(value); }
27+
private _matchSize = false;
28+
2129
constructor(public templateRef: TemplateRef<T>) {}
30+
31+
static ngAcceptInputType_matchSize: BooleanInput;
2232
}

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2890,6 +2890,37 @@ describe('CdkDrag', () => {
28902890
expect(preview.classList).toContain('custom-class');
28912891
}));
28922892

2893+
it('should be able to apply the size of the dragged element to a custom preview',
2894+
fakeAsync(() => {
2895+
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
2896+
fixture.componentInstance.matchPreviewSize = true;
2897+
fixture.detectChanges();
2898+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
2899+
const itemRect = item.getBoundingClientRect();
2900+
2901+
startDraggingViaMouse(fixture, item);
2902+
2903+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2904+
2905+
expect(preview).toBeTruthy();
2906+
expect(preview.style.width).toBe(`${itemRect.width}px`);
2907+
expect(preview.style.height).toBe(`${itemRect.height}px`);
2908+
}));
2909+
2910+
it('should preserve the pickup position if the custom preview inherits the size of the ' +
2911+
'dragged element', fakeAsync(() => {
2912+
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
2913+
fixture.componentInstance.matchPreviewSize = true;
2914+
fixture.detectChanges();
2915+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
2916+
2917+
startDraggingViaMouse(fixture, item, 50, 50);
2918+
2919+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2920+
2921+
expect(preview.style.transform).toBe('translate3d(8px, 33px, 0px)');
2922+
}));
2923+
28932924
it('should not throw when custom preview only has text', fakeAsync(() => {
28942925
const fixture = createComponent(DraggableInDropZoneWithCustomTextOnlyPreview);
28952926
fixture.detectChanges();
@@ -5009,10 +5040,11 @@ class DraggableInScrollableHorizontalDropZone extends DraggableInHorizontalDropZ
50095040
{{item}}
50105041
50115042
<ng-container *ngIf="renderCustomPreview">
5012-
<div
5013-
class="custom-preview"
5014-
style="width: 50px; height: 50px; background: purple;"
5015-
*cdkDragPreview>Custom preview</div>
5043+
<ng-template cdkDragPreview [matchSize]="matchPreviewSize">
5044+
<div
5045+
class="custom-preview"
5046+
style="width: 50px; height: 50px; background: purple;">Custom preview</div>
5047+
</ng-template>
50165048
</ng-container>
50175049
</div>
50185050
</div>
@@ -5024,6 +5056,7 @@ class DraggableInDropZoneWithCustomPreview {
50245056
items = ['Zero', 'One', 'Two', 'Three'];
50255057
boundarySelector: string;
50265058
renderCustomPreview = true;
5059+
matchPreviewSize = false;
50275060
previewClass: string | string[];
50285061
constrainPosition: (point: Point) => Point;
50295062
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
346346
const preview = this._previewTemplate ? {
347347
template: this._previewTemplate.templateRef,
348348
context: this._previewTemplate.data,
349+
matchSize: this._previewTemplate.matchSize,
349350
viewContainer: this._viewContainerRef
350351
} : null;
351352

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

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ interface DragHelperTemplate<T = any> {
6464
context: T;
6565
}
6666

67+
/** Template that can be used to create a drag preview element. */
68+
interface DragPreviewTemplate<T = any> extends DragHelperTemplate<T> {
69+
matchSize?: boolean;
70+
}
71+
6772
/** Point on the page or within an element. */
6873
export interface Point {
6974
x: number;
@@ -189,7 +194,7 @@ export class DragRef<T = any> {
189194
private _boundaryRect?: ClientRect;
190195

191196
/** Element that will be used as a template to create the draggable item's preview. */
192-
private _previewTemplate?: DragHelperTemplate | null;
197+
private _previewTemplate?: DragPreviewTemplate | null;
193198

194199
/** Template for placeholder element rendered to show where a draggable would be dropped. */
195200
private _placeholderTemplate?: DragHelperTemplate | null;
@@ -321,7 +326,7 @@ export class DragRef<T = any> {
321326
* Registers the template that should be used for the drag preview.
322327
* @param template Template that from which to stamp out the preview.
323328
*/
324-
withPreviewTemplate(template: DragHelperTemplate | null): this {
329+
withPreviewTemplate(template: DragPreviewTemplate | null): this {
325330
this._previewTemplate = template;
326331
return this;
327332
}
@@ -758,10 +763,12 @@ export class DragRef<T = any> {
758763
this._boundaryRect = this._boundaryElement.getBoundingClientRect();
759764
}
760765

761-
// If we have a custom preview template, the element won't be visible anyway so we avoid the
762-
// extra `getBoundingClientRect` calls and just move the preview next to the cursor.
763-
this._pickupPositionInElement = this._previewTemplate && this._previewTemplate.template ?
764-
{x: 0, y: 0} :
766+
// If we have a custom preview we can't know ahead of time how large it'll be so we position
767+
// it next to the cursor. The exception is when the consumer has opted into making the preview
768+
// the same size as the root element, in which case we do know the size.
769+
const previewTemplate = this._previewTemplate;
770+
this._pickupPositionInElement = previewTemplate && previewTemplate.template &&
771+
!previewTemplate.matchSize ? {x: 0, y: 0} :
765772
this._getPointerPositionInElement(referenceElement, event);
766773
const pointerPosition = this._pickupPositionOnPage = this._getPointerPositionOnPage(event);
767774
this._pointerDirectionDelta = {x: 0, y: 0};
@@ -861,16 +868,17 @@ export class DragRef<T = any> {
861868
previewConfig!.context);
862869
preview = getRootNode(viewRef, this._document);
863870
this._previewRef = viewRef;
864-
preview.style.transform =
865-
getTransform(this._pickupPositionOnPage.x, this._pickupPositionOnPage.y);
871+
872+
if (previewConfig!.matchSize) {
873+
matchElementSize(preview, this._rootElement);
874+
} else {
875+
preview.style.transform =
876+
getTransform(this._pickupPositionOnPage.x, this._pickupPositionOnPage.y);
877+
}
866878
} else {
867879
const element = this._rootElement;
868-
const elementRect = element.getBoundingClientRect();
869-
870880
preview = deepCloneNode(element);
871-
preview.style.width = `${elementRect.width}px`;
872-
preview.style.height = `${elementRect.height}px`;
873-
preview.style.transform = getTransform(elementRect.left, elementRect.top);
881+
matchElementSize(preview, element);
874882
}
875883

876884
extendStyles(preview.style, {
@@ -1279,3 +1287,16 @@ function getRootNode(viewRef: EmbeddedViewRef<any>, _document: Document): HTMLEl
12791287

12801288
return rootNode as HTMLElement;
12811289
}
1290+
1291+
/**
1292+
* Matches the target element's size to the source's size.
1293+
* @param target Element that needs to be resized.
1294+
* @param source Element whose size needs to be matched.
1295+
*/
1296+
function matchElementSize(target: HTMLElement, source: HTMLElement): void {
1297+
const sourceRect = source.getBoundingClientRect();
1298+
1299+
target.style.width = `${sourceRect.width}px`;
1300+
target.style.height = `${sourceRect.height}px`;
1301+
target.style.transform = getTransform(sourceRect.left, sourceRect.top);
1302+
}

tools/public_api_guard/cdk/drag-drop.d.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,12 @@ export declare class CdkDragPlaceholder<T = any> {
121121

122122
export declare class CdkDragPreview<T = any> {
123123
data: T;
124+
get matchSize(): boolean;
125+
set matchSize(value: boolean);
124126
templateRef: TemplateRef<T>;
125127
constructor(templateRef: TemplateRef<T>);
126-
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkDragPreview<any>, "ng-template[cdkDragPreview]", never, { "data": "data"; }, {}, never>;
128+
static ngAcceptInputType_matchSize: BooleanInput;
129+
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkDragPreview<any>, "ng-template[cdkDragPreview]", never, { "data": "data"; "matchSize": "matchSize"; }, {}, never>;
127130
static ɵfac: i0.ɵɵFactoryDef<CdkDragPreview<any>>;
128131
}
129132

@@ -305,7 +308,7 @@ export declare class DragRef<T = any> {
305308
withDirection(direction: Direction): this;
306309
withHandles(handles: (HTMLElement | ElementRef<HTMLElement>)[]): this;
307310
withPlaceholderTemplate(template: DragHelperTemplate | null): this;
308-
withPreviewTemplate(template: DragHelperTemplate | null): this;
311+
withPreviewTemplate(template: DragPreviewTemplate | null): this;
309312
withRootElement(rootElement: ElementRef<HTMLElement> | HTMLElement): this;
310313
}
311314

0 commit comments

Comments
 (0)