Skip to content

Commit 7508f46

Browse files
authored
fix(cdk/drag-drop): preserve initial transform inside drop list (#22422)
We have some logic that tries to preserve the original `transform` of an element, but we only apply it while dragging. These changes expand the support to include elements inside a drop list, as well as the preview and placeholder. Fixes #22407.
1 parent 6626fee commit 7508f46

File tree

4 files changed

+103
-14
lines changed

4 files changed

+103
-14
lines changed

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4331,6 +4331,48 @@ describe('CdkDrag', () => {
43314331
}).toThrowError(/^cdkDropList must be attached to an element node/);
43324332
}));
43334333

4334+
it('should preserve the original `transform` of items in the list', fakeAsync(() => {
4335+
const fixture = createComponent(DraggableInScrollableVerticalDropZone);
4336+
fixture.detectChanges();
4337+
const items = fixture.componentInstance.dragItems.map(item => item.element.nativeElement);
4338+
items.forEach(element => element.style.transform = 'rotate(180deg)');
4339+
const thirdItemRect = items[2].getBoundingClientRect();
4340+
const hasInitialTransform =
4341+
(element: HTMLElement) => element.style.transform.indexOf('rotate(180deg)') > -1;
4342+
4343+
startDraggingViaMouse(fixture, items[0]);
4344+
fixture.detectChanges();
4345+
const preview = document.querySelector('.cdk-drag-preview') as HTMLElement;
4346+
const placeholder = fixture.nativeElement.querySelector('.cdk-drag-placeholder');
4347+
4348+
expect(items.every(hasInitialTransform)).toBe(true,
4349+
'Expected items to preserve transform when dragging starts.');
4350+
expect(hasInitialTransform(preview)).toBe(true,
4351+
'Expected preview to preserve transform when dragging starts.');
4352+
expect(hasInitialTransform(placeholder)).toBe(true,
4353+
'Expected placeholder to preserve transform when dragging starts.');
4354+
4355+
dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1);
4356+
fixture.detectChanges();
4357+
expect(items.every(hasInitialTransform)).toBe(true,
4358+
'Expected items to preserve transform while dragging.');
4359+
expect(hasInitialTransform(preview)).toBe(true,
4360+
'Expected preview to preserve transform while dragging.');
4361+
expect(hasInitialTransform(placeholder)).toBe(true,
4362+
'Expected placeholder to preserve transform while dragging.');
4363+
4364+
dispatchMouseEvent(document, 'mouseup');
4365+
fixture.detectChanges();
4366+
flush();
4367+
fixture.detectChanges();
4368+
expect(items.every(hasInitialTransform)).toBe(true,
4369+
'Expected items to preserve transform when dragging stops.');
4370+
expect(hasInitialTransform(preview)).toBe(true,
4371+
'Expected preview to preserve transform when dragging stops.');
4372+
expect(hasInitialTransform(placeholder)).toBe(true,
4373+
'Expected placeholder to preserve transform when dragging stops.');
4374+
}));
4375+
43344376
});
43354377

43364378
describe('in a connected drop container', () => {

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

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import {coerceBooleanProperty, coerceElement} from '@angular/cdk/coercion';
1414
import {Subscription, Subject, Observable} from 'rxjs';
1515
import {DropListRefInternal as DropListRef} from './drop-list-ref';
1616
import {DragDropRegistry} from './drag-drop-registry';
17-
import {extendStyles, toggleNativeDragInteractions, toggleVisibility} from './drag-styling';
17+
import {
18+
combineTransforms,
19+
extendStyles,
20+
toggleNativeDragInteractions,
21+
toggleVisibility,
22+
} from './drag-styling';
1823
import {getTransformTransitionDurationInMs} from './transition-duration';
1924
import {getMutableClientRect, adjustClientRect} from './client-rect';
2025
import {ParentPositionTracker} from './parent-position-tracker';
@@ -792,7 +797,6 @@ export class DragRef<T = any> {
792797
if (dropContainer) {
793798
const element = this._rootElement;
794799
const parent = element.parentNode as HTMLElement;
795-
const preview = this._preview = this._createPreviewElement();
796800
const placeholder = this._placeholder = this._createPlaceholderElement();
797801
const anchor = this._anchor = this._anchor || this._document.createComment('');
798802

@@ -802,12 +806,20 @@ export class DragRef<T = any> {
802806
// Insert an anchor node so that we can restore the element's position in the DOM.
803807
parent.insertBefore(anchor, element);
804808

809+
// There's no risk of transforms stacking when inside a drop container so
810+
// we can keep the initial transform up to date any time dragging starts.
811+
this._initialTransform = element.style.transform || '';
812+
813+
// Create the preview after the initial transform has
814+
// been cached, because it can be affected by the transform.
815+
this._preview = this._createPreviewElement();
816+
805817
// We move the element out at the end of the body and we make it hidden, because keeping it in
806818
// place will throw off the consumer's `:last-child` selectors. We can't remove the element
807819
// from the DOM completely, because iOS will stop firing all subsequent events in the chain.
808820
toggleVisibility(element, false);
809821
this._document.body.appendChild(parent.replaceChild(placeholder, element));
810-
this._getPreviewInsertionPoint(parent, shadowRoot).appendChild(preview);
822+
this._getPreviewInsertionPoint(parent, shadowRoot).appendChild(this._preview);
811823
this.started.next({source: this}); // Emit before notifying the container.
812824
dropContainer.start();
813825
this._initialContainer = dropContainer;
@@ -906,7 +918,7 @@ export class DragRef<T = any> {
906918

907919
this._destroyPreview();
908920
this._destroyPlaceholder();
909-
this._boundaryRect = this._previewRect = undefined;
921+
this._boundaryRect = this._previewRect = this._initialTransform = undefined;
910922

911923
// Re-enter the NgZone since we bound `document` events on the outside.
912924
this._ngZone.run(() => {
@@ -972,8 +984,8 @@ export class DragRef<T = any> {
972984

973985
this._dropContainer!._startScrollingIfNecessary(rawX, rawY);
974986
this._dropContainer!._sortItem(this, x, y, this._pointerDirectionDelta);
975-
this._preview.style.transform =
976-
getTransform(x - this._pickupPositionInElement.x, y - this._pickupPositionInElement.y);
987+
this._applyPreviewTransform(
988+
x - this._pickupPositionInElement.x, y - this._pickupPositionInElement.y);
977989
}
978990

979991
/**
@@ -1005,6 +1017,10 @@ export class DragRef<T = any> {
10051017
const element = this._rootElement;
10061018
preview = deepCloneNode(element);
10071019
matchElementSize(preview, element.getBoundingClientRect());
1020+
1021+
if (this._initialTransform) {
1022+
preview.style.transform = this._initialTransform;
1023+
}
10081024
}
10091025

10101026
extendStyles(preview.style, {
@@ -1050,7 +1066,7 @@ export class DragRef<T = any> {
10501066
this._preview.classList.add('cdk-drag-animating');
10511067

10521068
// Move the preview to the placeholder position.
1053-
this._preview.style.transform = getTransform(placeholderRect.left, placeholderRect.top);
1069+
this._applyPreviewTransform(placeholderRect.left, placeholderRect.top);
10541070

10551071
// If the element doesn't have a `transition`, the `transitionend` event won't fire. Since
10561072
// we need to trigger a style recalculation in order for the `cdk-drag-animating` class to
@@ -1247,8 +1263,20 @@ export class DragRef<T = any> {
12471263
// Preserve the previous `transform` value, if there was one. Note that we apply our own
12481264
// transform before the user's, because things like rotation can affect which direction
12491265
// the element will be translated towards.
1250-
this._rootElement.style.transform = this._initialTransform ?
1251-
transform + ' ' + this._initialTransform : transform;
1266+
this._rootElement.style.transform = combineTransforms(transform, this._initialTransform);
1267+
}
1268+
1269+
/**
1270+
* Applies a `transform` to the preview, taking into account any existing transforms on it.
1271+
* @param x New transform value along the X axis.
1272+
* @param y New transform value along the Y axis.
1273+
*/
1274+
private _applyPreviewTransform(x: number, y: number) {
1275+
// Only apply the initial transform if the preview is a clone of the original element, otherwise
1276+
// it could be completely different and the transform might not make sense anymore.
1277+
const initialTransform = this._previewTemplate?.template ? undefined : this._initialTransform;
1278+
const transform = getTransform(x, y);
1279+
this._preview.style.transform = combineTransforms(transform, initialTransform);
12521280
}
12531281

12541282
/**

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,11 @@ export function toggleVisibility(element: HTMLElement, enable: boolean) {
7373
styles.top = styles.opacity = enable ? '' : '0';
7474
styles.left = enable ? '' : '-999em';
7575
}
76+
77+
/**
78+
* Combines a transform string with an optional other transform
79+
* that exited before the base transform was applied.
80+
*/
81+
export function combineTransforms(transform: string, initialTransform?: string): string {
82+
return initialTransform ? (transform + ' ' + initialTransform) : transform;
83+
}

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
isInsideClientRect,
2424
} from './client-rect';
2525
import {ParentPositionTracker} from './parent-position-tracker';
26-
import {DragCSSStyleDeclaration} from './drag-styling';
26+
import {combineTransforms, DragCSSStyleDeclaration} from './drag-styling';
2727

2828
/**
2929
* Proximity, as a ratio to width/height, at which a
@@ -48,6 +48,8 @@ interface CachedItemPosition {
4848
clientRect: ClientRect;
4949
/** Amount by which the item has been moved since dragging started. */
5050
offset: number;
51+
/** Inline transform that the drag item had when dragging started. */
52+
initialTransform: string;
5153
}
5254

5355
/** Vertical direction in which we can auto-scroll. */
@@ -513,10 +515,12 @@ export class DropListRef<T = any> {
513515
if (isHorizontal) {
514516
// Round the transforms since some browsers will
515517
// blur the elements, for sub-pixel transforms.
516-
elementToOffset.style.transform = `translate3d(${Math.round(sibling.offset)}px, 0, 0)`;
518+
elementToOffset.style.transform = combineTransforms(
519+
`translate3d(${Math.round(sibling.offset)}px, 0, 0)`, sibling.initialTransform);
517520
adjustClientRect(sibling.clientRect, 0, offset);
518521
} else {
519-
elementToOffset.style.transform = `translate3d(0, ${Math.round(sibling.offset)}px, 0)`;
522+
elementToOffset.style.transform = combineTransforms(
523+
`translate3d(0, ${Math.round(sibling.offset)}px, 0)`, sibling.initialTransform);
520524
adjustClientRect(sibling.clientRect, offset, 0);
521525
}
522526
});
@@ -622,7 +626,12 @@ export class DropListRef<T = any> {
622626

623627
this._itemPositions = this._activeDraggables.map(drag => {
624628
const elementToMeasure = drag.getVisibleElement();
625-
return {drag, offset: 0, clientRect: getMutableClientRect(elementToMeasure)};
629+
return {
630+
drag,
631+
offset: 0,
632+
initialTransform: elementToMeasure.style.transform || '',
633+
clientRect: getMutableClientRect(elementToMeasure),
634+
};
626635
}).sort((a, b) => {
627636
return isHorizontal ? a.clientRect.left - b.clientRect.left :
628637
a.clientRect.top - b.clientRect.top;
@@ -641,7 +650,9 @@ export class DropListRef<T = any> {
641650
const rootElement = item.getRootElement();
642651

643652
if (rootElement) {
644-
rootElement.style.transform = '';
653+
const initialTransform = this._itemPositions
654+
.find(current => current.drag === item)?.initialTransform;
655+
rootElement.style.transform = initialTransform || '';
645656
}
646657
});
647658
this._siblings.forEach(sibling => sibling._stopReceiving(this));

0 commit comments

Comments
 (0)