Skip to content

Commit 04b3933

Browse files
committed
fix(cdk/drag-drop): not detecting parent when projected using ngTemplateOutlet
Some logic was added in #21227 which would only `stopPropagation` when dragging if a parent drag item is detected. The problem is that we resolve the parent using DI which won't work if the item is projected through something like `ngTemplateOutlet`. These changes resolve the issue by falling back to resolving the parent through the DOM.
1 parent acb3f33 commit 04b3933

File tree

4 files changed

+112
-7
lines changed

4 files changed

+112
-7
lines changed

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5592,6 +5592,21 @@ describe('CdkDrag', () => {
55925592

55935593
expect(event.stopPropagation).toHaveBeenCalled();
55945594
}));
5595+
5596+
it('should stop event propagation when dragging item nested via ng-template', fakeAsync(() => {
5597+
const fixture = createComponent(NestedDragsThroughTemplate);
5598+
fixture.detectChanges();
5599+
const dragElement = fixture.componentInstance.item.nativeElement;
5600+
5601+
const event = createMouseEvent('mousedown');
5602+
spyOn(event, 'stopPropagation').and.callThrough();
5603+
5604+
dispatchEvent(dragElement, event);
5605+
fixture.detectChanges();
5606+
5607+
expect(event.stopPropagation).toHaveBeenCalled();
5608+
}));
5609+
55955610
});
55965611
});
55975612

@@ -6557,6 +6572,52 @@ class NestedDragsComponent {
65576572
itemDragReleasedSpy = jasmine.createSpy('item drag released spy');
65586573
}
65596574

6575+
@Component({
6576+
styles: [`
6577+
:host {
6578+
height: 400px;
6579+
width: 400px;
6580+
position: absolute;
6581+
}
6582+
.container {
6583+
height: 200px;
6584+
width: 200px;
6585+
position: absolute;
6586+
}
6587+
.item {
6588+
height: 50px;
6589+
width: 50px;
6590+
position: absolute;
6591+
}
6592+
`],
6593+
template: `
6594+
<div
6595+
cdkDrag
6596+
#container
6597+
class="container"
6598+
(cdkDragStarted)="containerDragStartedSpy($event)"
6599+
(cdkDragMoved)="containerDragMovedSpy($event)"
6600+
(cdkDragReleased)="containerDragReleasedSpy($event)">
6601+
<ng-container [ngTemplateOutlet]="itemTemplate"></ng-container>
6602+
</div>
6603+
6604+
<ng-template #itemTemplate>
6605+
<div
6606+
cdkDrag
6607+
class="item"
6608+
#item
6609+
(cdkDragStarted)="itemDragStartedSpy($event)"
6610+
(cdkDragMoved)="itemDragMovedSpy($event)"
6611+
(cdkDragReleased)="itemDragReleasedSpy($event)">
6612+
</div>
6613+
</ng-template>
6614+
`
6615+
})
6616+
class NestedDragsThroughTemplate {
6617+
@ViewChild('container') container: ElementRef;
6618+
@ViewChild('item') item: ElementRef;
6619+
}
6620+
65606621
@Component({
65616622
styles: [`
65626623
.drop-list {

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,22 @@ import {DragDrop} from '../drag-drop';
5656
import {CDK_DRAG_CONFIG, DragDropConfig, DragStartDelay, DragAxis} from './config';
5757
import {assertElementNode} from './assertions';
5858

59+
const DRAG_HOST_CLASS = 'cdk-drag';
60+
5961
/** Element that can be moved inside a CdkDropList container. */
6062
@Directive({
6163
selector: '[cdkDrag]',
6264
exportAs: 'cdkDrag',
6365
host: {
64-
'class': 'cdk-drag',
66+
'class': DRAG_HOST_CLASS,
6567
'[class.cdk-drag-disabled]': 'disabled',
6668
'[class.cdk-drag-dragging]': '_dragRef.isDragging()',
6769
},
6870
providers: [{provide: CDK_DRAG_PARENT, useExisting: CdkDrag}]
6971
})
7072
export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
7173
private _destroyed = new Subject<void>();
74+
private static _dragInstances: CdkDrag[] = [];
7275

7376
/** Reference to the underlying drag instance. */
7477
_dragRef: DragRef<CdkDrag<T>>;
@@ -193,17 +196,21 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
193196
@Optional() private _dir: Directionality, dragDrop: DragDrop,
194197
private _changeDetectorRef: ChangeDetectorRef,
195198
@Optional() @Self() @Inject(CDK_DRAG_HANDLE) private _selfHandle?: CdkDragHandle,
196-
@Optional() @SkipSelf() @Inject(CDK_DRAG_PARENT) parentDrag?: CdkDrag) {
199+
@Optional() @SkipSelf() @Inject(CDK_DRAG_PARENT) private _parentDrag?: CdkDrag) {
197200
this._dragRef = dragDrop.createDrag(element, {
198201
dragStartThreshold: config && config.dragStartThreshold != null ?
199202
config.dragStartThreshold : 5,
200203
pointerDirectionChangeThreshold: config && config.pointerDirectionChangeThreshold != null ?
201204
config.pointerDirectionChangeThreshold : 5,
202205
zIndex: config?.zIndex,
203-
parentDragRef: parentDrag?._dragRef
204206
});
205207
this._dragRef.data = this;
206208

209+
// We have to keep track of the drag instances in order to be able to match an element to
210+
// a drag instance. We can't go through the global registry of `DragRef`, because the root
211+
// element could be different.
212+
CdkDrag._dragInstances.push(this);
213+
207214
if (config) {
208215
this._assignDefaults(config);
209216
}
@@ -318,6 +325,10 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
318325
this.dropContainer.removeItem(this);
319326
}
320327

328+
const index = CdkDrag._dragInstances.indexOf(this);
329+
if (index > -1) {
330+
CdkDrag._dragInstances.splice(index, -1);
331+
}
321332
this._destroyed.next();
322333
this._destroyed.complete();
323334
this._dragRef.dispose();
@@ -392,6 +403,29 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
392403
}
393404
}
394405
});
406+
407+
// This only needs to be resolved once.
408+
ref.beforeStarted.pipe(take(1)).subscribe(() => {
409+
// If we managed to resolve a parent through DI, use it.
410+
if (this._parentDrag) {
411+
ref.withParent(this._parentDrag._dragRef);
412+
return;
413+
}
414+
415+
// Otherwise fall back to resolving the parent by looking up the DOM. This can happen if
416+
// the item was projected into another item by something like `ngTemplateOutlet`.
417+
let parent = this.element.nativeElement.parentElement;
418+
while (parent) {
419+
// `classList` needs to be null checked, because IE doesn't have it on some elements.
420+
if (parent.classList?.contains(DRAG_HOST_CLASS)) {
421+
ref.withParent(CdkDrag._dragInstances.find(drag => {
422+
return drag.element.nativeElement === parent;
423+
})?._dragRef || null);
424+
break;
425+
}
426+
parent = parent.parentElement;
427+
}
428+
});
395429
}
396430

397431
/** Handles the events from the underlying `DragRef`. */

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ export class DragRef<T = any> {
230230
/** Layout direction of the item. */
231231
private _direction: Direction = 'ltr';
232232

233+
/** Ref that the current drag item is nested in. */
234+
private _parentDragRef: DragRef<unknown> | null;
235+
233236
/**
234237
* Cached shadow root that the element is placed in. `null` means that the element isn't in
235238
* the shadow DOM and `undefined` means that it hasn't been resolved yet. Should be read via
@@ -324,7 +327,7 @@ export class DragRef<T = any> {
324327
private _viewportRuler: ViewportRuler,
325328
private _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>) {
326329

327-
this.withRootElement(element);
330+
this.withRootElement(element).withParent(_config.parentDragRef || null);
328331
this._parentPositions = new ParentPositionTracker(_document, _viewportRuler);
329332
_dragDropRegistry.registerDragItem(this);
330333
}
@@ -430,6 +433,12 @@ export class DragRef<T = any> {
430433
return this;
431434
}
432435

436+
/** Sets the parent ref that the ref is nested in. */
437+
withParent(parent: DragRef<unknown> | null): this {
438+
this._parentDragRef = parent;
439+
return this;
440+
}
441+
433442
/** Removes the dragging functionality from the DOM element. */
434443
dispose() {
435444
this._removeRootElementListeners(this._rootElement);
@@ -461,7 +470,7 @@ export class DragRef<T = any> {
461470
this._resizeSubscription.unsubscribe();
462471
this._parentPositions.clear();
463472
this._boundaryElement = this._rootElement = this._ownerSVGElement = this._placeholderTemplate =
464-
this._previewTemplate = this._anchor = null!;
473+
this._previewTemplate = this._anchor = this._parentDragRef = null!;
465474
}
466475

467476
/** Checks whether the element is currently being dragged. */
@@ -790,7 +799,7 @@ export class DragRef<T = any> {
790799
private _initializeDragSequence(referenceElement: HTMLElement, event: MouseEvent | TouchEvent) {
791800
// Stop propagation if the item is inside another
792801
// draggable so we don't start multiple drag sequences.
793-
if (this._config.parentDragRef) {
802+
if (this._parentDragRef) {
794803
event.stopPropagation();
795804
}
796805

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
4242
constructor(
4343
element: ElementRef<HTMLElement>,
4444
dropContainer: CdkDropList,
45-
_document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, config: DragDropConfig, _dir: Directionality, dragDrop: DragDrop, _changeDetectorRef: ChangeDetectorRef, _selfHandle?: CdkDragHandle | undefined, parentDrag?: CdkDrag);
45+
_document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, config: DragDropConfig, _dir: Directionality, dragDrop: DragDrop, _changeDetectorRef: ChangeDetectorRef, _selfHandle?: CdkDragHandle | undefined, _parentDrag?: CdkDrag<any> | undefined);
4646
getFreeDragPosition(): {
4747
readonly x: number;
4848
readonly y: number;
@@ -318,6 +318,7 @@ export declare class DragRef<T = any> {
318318
withBoundaryElement(boundaryElement: ElementRef<HTMLElement> | HTMLElement | null): this;
319319
withDirection(direction: Direction): this;
320320
withHandles(handles: (HTMLElement | ElementRef<HTMLElement>)[]): this;
321+
withParent(parent: DragRef<unknown> | null): this;
321322
withPlaceholderTemplate(template: DragHelperTemplate | null): this;
322323
withPreviewTemplate(template: DragPreviewTemplate | null): this;
323324
withRootElement(rootElement: ElementRef<HTMLElement> | HTMLElement): this;

0 commit comments

Comments
 (0)