Skip to content

Commit 622057a

Browse files
committed
fix(cdk/drag-drop): resolve projected handles
Currently the `cdkDragHandle` directive registers itself with the parent by resolving it through DI. This doesn't work if the directive is declared in a separate embedded view (e.g. `ng-template`) that is then projected into the draggable element. It can be problematic when adding dragging support to a `mat-table`. These changes fix the issue by falling back to resolving the draggable directive through the DOM. Fixes #29475. (cherry picked from commit a141c22)
1 parent bc5217b commit 622057a

File tree

3 files changed

+60
-2
lines changed

3 files changed

+60
-2
lines changed

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {
10+
AfterViewInit,
1011
Directive,
1112
ElementRef,
1213
InjectionToken,
@@ -19,6 +20,7 @@ import {Subject} from 'rxjs';
1920
import type {CdkDrag} from './drag';
2021
import {CDK_DRAG_PARENT} from '../drag-parent';
2122
import {assertElementNode} from './assertions';
23+
import {DragDropRegistry} from '../drag-drop-registry';
2224

2325
/**
2426
* Injection token that can be used to reference instances of `CdkDragHandle`. It serves as
@@ -35,10 +37,11 @@ export const CDK_DRAG_HANDLE = new InjectionToken<CdkDragHandle>('CdkDragHandle'
3537
},
3638
providers: [{provide: CDK_DRAG_HANDLE, useExisting: CdkDragHandle}],
3739
})
38-
export class CdkDragHandle implements OnDestroy {
40+
export class CdkDragHandle implements AfterViewInit, OnDestroy {
3941
element = inject<ElementRef<HTMLElement>>(ElementRef);
4042

4143
private _parentDrag = inject<CdkDrag>(CDK_DRAG_PARENT, {optional: true, skipSelf: true});
44+
private _dragDropRegistry = inject(DragDropRegistry);
4245

4346
/** Emits when the state of the handle has changed. */
4447
readonly _stateChanges = new Subject<CdkDragHandle>();
@@ -64,6 +67,21 @@ export class CdkDragHandle implements OnDestroy {
6467
this._parentDrag?._addHandle(this);
6568
}
6669

70+
ngAfterViewInit() {
71+
if (!this._parentDrag) {
72+
let parent = this.element.nativeElement.parentElement;
73+
while (parent) {
74+
const ref = this._dragDropRegistry.getDragDirectiveForNode(parent);
75+
if (ref) {
76+
this._parentDrag = ref;
77+
ref._addHandle(this);
78+
break;
79+
}
80+
parent = parent.parentElement;
81+
}
82+
}
83+
}
84+
6785
ngOnDestroy() {
6886
this._parentDrag?._removeHandle(this);
6987
this._stateChanges.complete();

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ViewEncapsulation,
1010
signal,
1111
} from '@angular/core';
12+
import {NgTemplateOutlet} from '@angular/common';
1213
import {fakeAsync, flush, tick} from '@angular/core/testing';
1314
import {
1415
dispatchEvent,
@@ -1631,6 +1632,25 @@ describe('Standalone CdkDrag', () => {
16311632
.toBe('translate3d(50px, 100px, 0px)');
16321633
}));
16331634

1635+
it('should be able to drag with a handle that is defined in a separate embedded view', fakeAsync(() => {
1636+
const fixture = createComponent(StandaloneDraggableWithExternalTemplateHandle);
1637+
fixture.detectChanges();
1638+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
1639+
const handle = fixture.nativeElement.querySelector('.handle');
1640+
1641+
expect(dragElement.style.transform).toBeFalsy();
1642+
dragElementViaMouse(fixture, dragElement, 50, 100);
1643+
1644+
expect(dragElement.style.transform)
1645+
.withContext('Expected not to be able to drag the element by itself.')
1646+
.toBeFalsy();
1647+
1648+
dragElementViaMouse(fixture, handle, 50, 100);
1649+
expect(dragElement.style.transform)
1650+
.withContext('Expected to drag the element by its handle.')
1651+
.toBe('translate3d(50px, 100px, 0px)');
1652+
}));
1653+
16341654
it('should disable the tap highlight while dragging via the handle', fakeAsync(() => {
16351655
// This test is irrelevant if the browser doesn't support styling the tap highlight color.
16361656
if (!('webkitTapHighlightColor' in document.body.style)) {
@@ -2010,3 +2030,21 @@ class DraggableNgContainerWithAlternateRoot {
20102030
class PlainStandaloneDraggable {
20112031
@ViewChild(CdkDrag) dragInstance: CdkDrag;
20122032
}
2033+
2034+
@Component({
2035+
template: `
2036+
<div #dragElement cdkDrag
2037+
style="width: 100px; height: 100px; background: red; position: relative">
2038+
<ng-container [ngTemplateOutlet]="template"/>
2039+
</div>
2040+
2041+
<ng-template #template>
2042+
<div cdkDragHandle class="handle" style="width: 10px; height: 10px; background: green;"></div>
2043+
</ng-template>
2044+
`,
2045+
standalone: true,
2046+
imports: [CdkDrag, CdkDragHandle, NgTemplateOutlet],
2047+
})
2048+
class StandaloneDraggableWithExternalTemplateHandle {
2049+
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
2050+
}

tools/public_api_guard/cdk/drag-drop.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export interface CdkDragExit<T = any, I = T> {
150150
}
151151

152152
// @public
153-
export class CdkDragHandle implements OnDestroy {
153+
export class CdkDragHandle implements AfterViewInit, OnDestroy {
154154
constructor(...args: unknown[]);
155155
get disabled(): boolean;
156156
set disabled(value: boolean);
@@ -159,6 +159,8 @@ export class CdkDragHandle implements OnDestroy {
159159
// (undocumented)
160160
static ngAcceptInputType_disabled: unknown;
161161
// (undocumented)
162+
ngAfterViewInit(): void;
163+
// (undocumented)
162164
ngOnDestroy(): void;
163165
readonly _stateChanges: Subject<CdkDragHandle>;
164166
// (undocumented)

0 commit comments

Comments
 (0)