Skip to content

Commit 74b3441

Browse files
authored
feat(drag-drop): add option to match size of dragged element in custom preview (#18362)
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 591ac9c commit 74b3441

File tree

6 files changed

+91
-19
lines changed

6 files changed

+91
-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
@@ -2931,6 +2931,37 @@ describe('CdkDrag', () => {
29312931
expect(preview.classList).toContain('custom-class');
29322932
}));
29332933

2934+
it('should be able to apply the size of the dragged element to a custom preview',
2935+
fakeAsync(() => {
2936+
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
2937+
fixture.componentInstance.matchPreviewSize = true;
2938+
fixture.detectChanges();
2939+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
2940+
const itemRect = item.getBoundingClientRect();
2941+
2942+
startDraggingViaMouse(fixture, item);
2943+
2944+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2945+
2946+
expect(preview).toBeTruthy();
2947+
expect(preview.style.width).toBe(`${itemRect.width}px`);
2948+
expect(preview.style.height).toBe(`${itemRect.height}px`);
2949+
}));
2950+
2951+
it('should preserve the pickup position if the custom preview inherits the size of the ' +
2952+
'dragged element', fakeAsync(() => {
2953+
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
2954+
fixture.componentInstance.matchPreviewSize = true;
2955+
fixture.detectChanges();
2956+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
2957+
2958+
startDraggingViaMouse(fixture, item, 50, 50);
2959+
2960+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2961+
2962+
expect(preview.style.transform).toBe('translate3d(8px, 33px, 0px)');
2963+
}));
2964+
29342965
it('should not throw when custom preview only has text', fakeAsync(() => {
29352966
const fixture = createComponent(DraggableInDropZoneWithCustomTextOnlyPreview);
29362967
fixture.detectChanges();
@@ -5050,10 +5081,11 @@ class DraggableInScrollableHorizontalDropZone extends DraggableInHorizontalDropZ
50505081
{{item}}
50515082
50525083
<ng-container *ngIf="renderCustomPreview">
5053-
<div
5054-
class="custom-preview"
5055-
style="width: 50px; height: 50px; background: purple;"
5056-
*cdkDragPreview>Custom preview</div>
5084+
<ng-template cdkDragPreview [matchSize]="matchPreviewSize">
5085+
<div
5086+
class="custom-preview"
5087+
style="width: 50px; height: 50px; background: purple;">Custom preview</div>
5088+
</ng-template>
50575089
</ng-container>
50585090
</div>
50595091
</div>
@@ -5065,6 +5097,7 @@ class DraggableInDropZoneWithCustomPreview {
50655097
items = ['Zero', 'One', 'Two', 'Three'];
50665098
boundarySelector: string;
50675099
renderCustomPreview = true;
5100+
matchPreviewSize = false;
50685101
previewClass: string | string[];
50695102
constrainPosition: (point: Point) => Point;
50705103
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
362362
const preview = this._previewTemplate ? {
363363
template: this._previewTemplate.templateRef,
364364
context: this._previewTemplate.data,
365+
matchSize: this._previewTemplate.matchSize,
365366
viewContainer: this._viewContainerRef
366367
} : null;
367368

src/cdk/drag-drop/drag-drop.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ restrict the user to only be able to do so using a handle element, you can do it
118118
When a `cdkDrag` element is picked up, it will create a preview element visible while dragging.
119119
By default, this will be a clone of the original element positioned next to the user's cursor.
120120
This preview can be customized, though, by providing a custom template via `*cdkDragPreview`.
121+
Using the default configuration the custom preview won't match the size of the original dragged
122+
element, because the CDK doesn't make assumptions about the element's content. If you want the
123+
size to be matched, you can pass `true` to the `matchSize` input.
124+
121125
Note that the cloned element will remove its `id` attribute in order to avoid having multiple
122126
elements with the same `id` on the page. This will cause any CSS that targets that `id` not
123127
to be applied.

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;
@@ -192,7 +197,7 @@ export class DragRef<T = any> {
192197
private _boundaryRect?: ClientRect;
193198

194199
/** Element that will be used as a template to create the draggable item's preview. */
195-
private _previewTemplate?: DragHelperTemplate | null;
200+
private _previewTemplate?: DragPreviewTemplate | null;
196201

197202
/** Template for placeholder element rendered to show where a draggable would be dropped. */
198203
private _placeholderTemplate?: DragHelperTemplate | null;
@@ -332,7 +337,7 @@ export class DragRef<T = any> {
332337
* Registers the template that should be used for the drag preview.
333338
* @param template Template that from which to stamp out the preview.
334339
*/
335-
withPreviewTemplate(template: DragHelperTemplate | null): this {
340+
withPreviewTemplate(template: DragPreviewTemplate | null): this {
336341
this._previewTemplate = template;
337342
return this;
338343
}
@@ -772,10 +777,12 @@ export class DragRef<T = any> {
772777
this._boundaryRect = this._boundaryElement.getBoundingClientRect();
773778
}
774779

775-
// If we have a custom preview template, the element won't be visible anyway so we avoid the
776-
// extra `getBoundingClientRect` calls and just move the preview next to the cursor.
777-
this._pickupPositionInElement = this._previewTemplate && this._previewTemplate.template ?
778-
{x: 0, y: 0} :
780+
// If we have a custom preview we can't know ahead of time how large it'll be so we position
781+
// it next to the cursor. The exception is when the consumer has opted into making the preview
782+
// the same size as the root element, in which case we do know the size.
783+
const previewTemplate = this._previewTemplate;
784+
this._pickupPositionInElement = previewTemplate && previewTemplate.template &&
785+
!previewTemplate.matchSize ? {x: 0, y: 0} :
779786
this._getPointerPositionInElement(referenceElement, event);
780787
const pointerPosition = this._pickupPositionOnPage = this._getPointerPositionOnPage(event);
781788
this._pointerDirectionDelta = {x: 0, y: 0};
@@ -879,16 +886,17 @@ export class DragRef<T = any> {
879886
previewConfig!.context);
880887
preview = getRootNode(viewRef, this._document);
881888
this._previewRef = viewRef;
882-
preview.style.transform =
883-
getTransform(this._pickupPositionOnPage.x, this._pickupPositionOnPage.y);
889+
890+
if (previewConfig!.matchSize) {
891+
matchElementSize(preview, this._rootElement);
892+
} else {
893+
preview.style.transform =
894+
getTransform(this._pickupPositionOnPage.x, this._pickupPositionOnPage.y);
895+
}
884896
} else {
885897
const element = this._rootElement;
886-
const elementRect = element.getBoundingClientRect();
887-
888898
preview = deepCloneNode(element);
889-
preview.style.width = `${elementRect.width}px`;
890-
preview.style.height = `${elementRect.height}px`;
891-
preview.style.transform = getTransform(elementRect.left, elementRect.top);
899+
matchElementSize(preview, element);
892900
}
893901

894902
extendStyles(preview.style, {
@@ -1297,3 +1305,16 @@ function getRootNode(viewRef: EmbeddedViewRef<any>, _document: Document): HTMLEl
12971305

12981306
return rootNode as HTMLElement;
12991307
}
1308+
1309+
/**
1310+
* Matches the target element's size to the source's size.
1311+
* @param target Element that needs to be resized.
1312+
* @param source Element whose size needs to be matched.
1313+
*/
1314+
function matchElementSize(target: HTMLElement, source: HTMLElement): void {
1315+
const sourceRect = source.getBoundingClientRect();
1316+
1317+
target.style.width = `${sourceRect.width}px`;
1318+
target.style.height = `${sourceRect.height}px`;
1319+
target.style.transform = getTransform(sourceRect.left, sourceRect.top);
1320+
}

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

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

0 commit comments

Comments
 (0)