Skip to content

Commit b2c051d

Browse files
authoredJul 8, 2024··
feat(cdk/drag-drop): add input to specify dragged item scale (#29392)
In some cases the parent of the dragged element might be scaled (e.g. when implementing zoom) which can throw off the positioning. Detecting this would be expensive, because we'd have to check the entire DOM tree so instead these changes add an input so the user can specify the scale amount.
1 parent e95d88c commit b2c051d

File tree

7 files changed

+89
-26
lines changed

7 files changed

+89
-26
lines changed
 

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

+14
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
AfterViewInit,
3131
inject,
3232
Injector,
33+
numberAttribute,
3334
} from '@angular/core';
3435
import {coerceElement, coerceNumberProperty} from '@angular/cdk/coercion';
3536
import {BehaviorSubject, Observable, Observer, Subject, merge} from 'rxjs';
@@ -159,6 +160,13 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
159160
*/
160161
@Input('cdkDragPreviewContainer') previewContainer: PreviewContainer;
161162

163+
/**
164+
* If the parent of the dragged element has a `scale` transform, it can throw off the
165+
* positioning when the user starts dragging. Use this input to notify the CDK of the scale.
166+
*/
167+
@Input({alias: 'cdkDragScale', transform: numberAttribute})
168+
scale: number = 1;
169+
162170
/** Emits when the user starts dragging the item. */
163171
@Output('cdkDragStarted') readonly started: EventEmitter<CdkDragStart> =
164172
new EventEmitter<CdkDragStart>();
@@ -261,6 +269,11 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
261269
if (dropContainer) {
262270
this._dragRef._withDropContainer(dropContainer._dropListRef);
263271
dropContainer.addItem(this);
272+
273+
// The drop container reads this so we need to sync it here.
274+
dropContainer._dropListRef.beforeStarted.pipe(takeUntil(this._destroyed)).subscribe(() => {
275+
this._dragRef.scale = this.scale;
276+
});
264277
}
265278

266279
this._syncInputs(this._dragRef);
@@ -448,6 +461,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
448461

449462
ref.disabled = this.disabled;
450463
ref.lockAxis = this.lockAxis;
464+
ref.scale = this.scale;
451465
ref.dragStartDelay =
452466
typeof dragStartDelay === 'object' && dragStartDelay
453467
? dragStartDelay

‎src/cdk/drag-drop/directives/drop-list-shared.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5006,6 +5006,7 @@ const DROP_ZONE_FIXTURE_TEMPLATE = `
50065006
[cdkDragBoundary]="boundarySelector"
50075007
[cdkDragPreviewClass]="previewClass"
50085008
[cdkDragPreviewContainer]="previewContainer"
5009+
[cdkDragScale]="scale"
50095010
[style.height.px]="item.height"
50105011
[style.margin-bottom.px]="item.margin"
50115012
(cdkDragStarted)="startedSpy($event)"
@@ -5041,6 +5042,7 @@ export class DraggableInDropZone implements AfterViewInit {
50415042
previewContainer: PreviewContainer = 'global';
50425043
dropDisabled = signal(false);
50435044
dropLockAxis = signal<DragAxis | undefined>(undefined);
5045+
scale = 1;
50445046

50455047
constructor(protected _elementRef: ElementRef) {}
50465048

‎src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts

+25
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,29 @@ describe('Single-axis drop list', () => {
311311

312312
dispatchMouseEvent(document, 'mouseup');
313313
}));
314+
315+
it('should lay out the elements correctly when scaled', fakeAsync(() => {
316+
const fixture = createComponent(DraggableInDropZone);
317+
fixture.componentInstance.scale = 0.5;
318+
fixture.detectChanges();
319+
320+
const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement);
321+
const {top, left} = items[0].getBoundingClientRect();
322+
323+
startDraggingViaMouse(fixture, items[0], left, top);
324+
325+
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
326+
const target = items[1];
327+
const targetRect = target.getBoundingClientRect();
328+
329+
dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5);
330+
fixture.detectChanges();
331+
332+
expect(placeholder.style.transform).toBe(`translate3d(0px, ${ITEM_HEIGHT * 2}px, 0px)`);
333+
expect(target.style.transform).toBe(`translate3d(0px, ${-ITEM_HEIGHT * 2}px, 0px)`);
334+
335+
dispatchMouseEvent(document, 'mouseup');
336+
fixture.detectChanges();
337+
flush();
338+
}));
314339
});

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

+31-22
Original file line numberDiff line numberDiff line change
@@ -1470,34 +1470,41 @@ describe('Standalone CdkDrag', () => {
14701470
cleanup();
14711471
}));
14721472

1473-
it(
1474-
'should update the free drag position if the user moves their pointer after the page ' +
1475-
'is scrolled',
1476-
fakeAsync(() => {
1477-
const fixture = createComponent(StandaloneDraggable);
1478-
fixture.detectChanges();
1473+
it('should update the free drag position if the user moves their pointer after the page is scrolled', fakeAsync(() => {
1474+
const fixture = createComponent(StandaloneDraggable);
1475+
fixture.detectChanges();
14791476

1480-
const cleanup = makeScrollable();
1481-
const dragElement = fixture.componentInstance.dragElement.nativeElement;
1477+
const cleanup = makeScrollable();
1478+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
14821479

1483-
expect(dragElement.style.transform).toBeFalsy();
1484-
startDraggingViaMouse(fixture, dragElement, 0, 0);
1485-
dispatchMouseEvent(document, 'mousemove', 50, 100);
1486-
fixture.detectChanges();
1480+
expect(dragElement.style.transform).toBeFalsy();
1481+
startDraggingViaMouse(fixture, dragElement, 0, 0);
1482+
dispatchMouseEvent(document, 'mousemove', 50, 100);
1483+
fixture.detectChanges();
14871484

1488-
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
1485+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
14891486

1490-
scrollTo(0, 500);
1491-
dispatchFakeEvent(document, 'scroll');
1492-
fixture.detectChanges();
1493-
dispatchMouseEvent(document, 'mousemove', 50, 200);
1494-
fixture.detectChanges();
1487+
scrollTo(0, 500);
1488+
dispatchFakeEvent(document, 'scroll');
1489+
fixture.detectChanges();
1490+
dispatchMouseEvent(document, 'mousemove', 50, 200);
1491+
fixture.detectChanges();
14951492

1496-
expect(dragElement.style.transform).toBe('translate3d(50px, 700px, 0px)');
1493+
expect(dragElement.style.transform).toBe('translate3d(50px, 700px, 0px)');
14971494

1498-
cleanup();
1499-
}),
1500-
);
1495+
cleanup();
1496+
}));
1497+
1498+
it('should account for scale when moving the element', fakeAsync(() => {
1499+
const fixture = createComponent(StandaloneDraggable);
1500+
fixture.componentInstance.scale = 0.5;
1501+
fixture.detectChanges();
1502+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
1503+
1504+
expect(dragElement.style.transform).toBeFalsy();
1505+
dragElementViaMouse(fixture, dragElement, 50, 100);
1506+
expect(dragElement.style.transform).toBe('translate3d(100px, 200px, 0px)');
1507+
}));
15011508

15021509
describe('with a handle', () => {
15031510
it('should not be able to drag the entire element if it has a handle', fakeAsync(() => {
@@ -1718,6 +1725,7 @@ describe('Standalone CdkDrag', () => {
17181725
[cdkDragFreeDragPosition]="freeDragPosition"
17191726
[cdkDragDisabled]="dragDisabled()"
17201727
[cdkDragLockAxis]="dragLockAxis()"
1728+
[cdkDragScale]="scale"
17211729
(cdkDragStarted)="startedSpy($event)"
17221730
(cdkDragReleased)="releasedSpy($event)"
17231731
(cdkDragEnded)="endedSpy($event)"
@@ -1745,6 +1753,7 @@ class StandaloneDraggable {
17451753
freeDragPosition?: {x: number; y: number};
17461754
dragDisabled = signal(false);
17471755
dragLockAxis = signal<DragAxis | undefined>(undefined);
1756+
scale = 1;
17481757
}
17491758

17501759
@Component({

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,12 @@ export class DragRef<T = any> {
288288
/** Class to be added to the preview element. */
289289
previewClass: string | string[] | undefined;
290290

291+
/**
292+
* If the parent of the dragged element has a `scale` transform, it can throw off the
293+
* positioning when the user starts dragging. Use this input to notify the CDK of the scale.
294+
*/
295+
scale: number = 1;
296+
291297
/** Whether starting to drag this element is disabled. */
292298
get disabled(): boolean {
293299
return this._disabled || !!(this._dropContainer && this._dropContainer.disabled);
@@ -1288,7 +1294,8 @@ export class DragRef<T = any> {
12881294
* @param y New transform value along the Y axis.
12891295
*/
12901296
private _applyRootElementTransform(x: number, y: number) {
1291-
const transform = getTransform(x, y);
1297+
const scale = 1 / this.scale;
1298+
const transform = getTransform(x * scale, y * scale);
12921299
const styles = this._rootElement.style;
12931300

12941301
// Cache the previous transform amount only after the first drag sequence, because

‎src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ export class SingleAxisSortStrategy implements DropListSortStrategy {
128128
// Update the offset to reflect the new position.
129129
sibling.offset += offset;
130130

131+
const transformAmount = Math.round(sibling.offset * (1 / sibling.drag.scale));
132+
131133
// Since we're moving the items with a `transform`, we need to adjust their cached
132134
// client rects to reflect their new position, as well as swap their positions in the cache.
133135
// Note that we shouldn't use `getBoundingClientRect` here to update the cache, because the
@@ -136,13 +138,13 @@ export class SingleAxisSortStrategy implements DropListSortStrategy {
136138
// Round the transforms since some browsers will
137139
// blur the elements, for sub-pixel transforms.
138140
elementToOffset.style.transform = combineTransforms(
139-
`translate3d(${Math.round(sibling.offset)}px, 0, 0)`,
141+
`translate3d(${transformAmount}px, 0, 0)`,
140142
sibling.initialTransform,
141143
);
142144
adjustDomRect(sibling.clientRect, 0, offset);
143145
} else {
144146
elementToOffset.style.transform = combineTransforms(
145-
`translate3d(0, ${Math.round(sibling.offset)}px, 0)`,
147+
`translate3d(0, ${transformAmount}px, 0)`,
146148
sibling.initialTransform,
147149
);
148150
adjustDomRect(sibling.clientRect, offset, 0);

‎tools/public_api_guard/cdk/drag-drop.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
7676
// (undocumented)
7777
static ngAcceptInputType_disabled: unknown;
7878
// (undocumented)
79+
static ngAcceptInputType_scale: unknown;
80+
// (undocumented)
7981
ngAfterViewInit(): void;
8082
// (undocumented)
8183
ngOnChanges(changes: SimpleChanges): void;
@@ -92,14 +94,15 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
9294
// (undocumented)
9395
_resetPreviewTemplate(preview: CdkDragPreview): void;
9496
rootElementSelector: string;
97+
scale: number;
9598
setFreeDragPosition(value: Point): void;
9699
// (undocumented)
97100
_setPlaceholderTemplate(placeholder: CdkDragPlaceholder): void;
98101
// (undocumented)
99102
_setPreviewTemplate(preview: CdkDragPreview): void;
100103
readonly started: EventEmitter<CdkDragStart>;
101104
// (undocumented)
102-
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDrag<any>, "[cdkDrag]", ["cdkDrag"], { "data": { "alias": "cdkDragData"; "required": false; }; "lockAxis": { "alias": "cdkDragLockAxis"; "required": false; }; "rootElementSelector": { "alias": "cdkDragRootElement"; "required": false; }; "boundaryElement": { "alias": "cdkDragBoundary"; "required": false; }; "dragStartDelay": { "alias": "cdkDragStartDelay"; "required": false; }; "freeDragPosition": { "alias": "cdkDragFreeDragPosition"; "required": false; }; "disabled": { "alias": "cdkDragDisabled"; "required": false; }; "constrainPosition": { "alias": "cdkDragConstrainPosition"; "required": false; }; "previewClass": { "alias": "cdkDragPreviewClass"; "required": false; }; "previewContainer": { "alias": "cdkDragPreviewContainer"; "required": false; }; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, never, never, true, never>;
105+
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDrag<any>, "[cdkDrag]", ["cdkDrag"], { "data": { "alias": "cdkDragData"; "required": false; }; "lockAxis": { "alias": "cdkDragLockAxis"; "required": false; }; "rootElementSelector": { "alias": "cdkDragRootElement"; "required": false; }; "boundaryElement": { "alias": "cdkDragBoundary"; "required": false; }; "dragStartDelay": { "alias": "cdkDragStartDelay"; "required": false; }; "freeDragPosition": { "alias": "cdkDragFreeDragPosition"; "required": false; }; "disabled": { "alias": "cdkDragDisabled"; "required": false; }; "constrainPosition": { "alias": "cdkDragConstrainPosition"; "required": false; }; "previewClass": { "alias": "cdkDragPreviewClass"; "required": false; }; "previewContainer": { "alias": "cdkDragPreviewContainer"; "required": false; }; "scale": { "alias": "cdkDragScale"; "required": false; }; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, never, never, true, never>;
103106
// (undocumented)
104107
static ɵfac: i0.ɵɵFactoryDeclaration<CdkDrag<any>, [null, { optional: true; skipSelf: true; }, null, null, null, { optional: true; }, { optional: true; }, null, null, { optional: true; self: true; }, { optional: true; skipSelf: true; }]>;
105108
}
@@ -440,6 +443,7 @@ export class DragRef<T = any> {
440443
event: MouseEvent | TouchEvent;
441444
}>;
442445
reset(): void;
446+
scale: number;
443447
setFreeDragPosition(value: Point): this;
444448
_sortFromLastPointerPosition(): void;
445449
readonly started: Subject<{

0 commit comments

Comments
 (0)
Please sign in to comment.