Skip to content

Commit

Permalink
fix(cdk/drag-drop): resolve projected handles
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
crisbeto committed Dec 12, 2024
1 parent bc5217b commit 622057a
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 2 deletions.
20 changes: 19 additions & 1 deletion src/cdk/drag-drop/directives/drag-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import {
AfterViewInit,
Directive,
ElementRef,
InjectionToken,
Expand All @@ -19,6 +20,7 @@ import {Subject} from 'rxjs';
import type {CdkDrag} from './drag';
import {CDK_DRAG_PARENT} from '../drag-parent';
import {assertElementNode} from './assertions';
import {DragDropRegistry} from '../drag-drop-registry';

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

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

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

ngAfterViewInit() {
if (!this._parentDrag) {
let parent = this.element.nativeElement.parentElement;
while (parent) {
const ref = this._dragDropRegistry.getDragDirectiveForNode(parent);
if (ref) {
this._parentDrag = ref;
ref._addHandle(this);
break;
}
parent = parent.parentElement;
}
}
}

ngOnDestroy() {
this._parentDrag?._removeHandle(this);
this._stateChanges.complete();
Expand Down
38 changes: 38 additions & 0 deletions src/cdk/drag-drop/directives/standalone-drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ViewEncapsulation,
signal,
} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {fakeAsync, flush, tick} from '@angular/core/testing';
import {
dispatchEvent,
Expand Down Expand Up @@ -1631,6 +1632,25 @@ describe('Standalone CdkDrag', () => {
.toBe('translate3d(50px, 100px, 0px)');
}));

it('should be able to drag with a handle that is defined in a separate embedded view', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggableWithExternalTemplateHandle);
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;
const handle = fixture.nativeElement.querySelector('.handle');

expect(dragElement.style.transform).toBeFalsy();
dragElementViaMouse(fixture, dragElement, 50, 100);

expect(dragElement.style.transform)
.withContext('Expected not to be able to drag the element by itself.')
.toBeFalsy();

dragElementViaMouse(fixture, handle, 50, 100);
expect(dragElement.style.transform)
.withContext('Expected to drag the element by its handle.')
.toBe('translate3d(50px, 100px, 0px)');
}));

it('should disable the tap highlight while dragging via the handle', fakeAsync(() => {
// This test is irrelevant if the browser doesn't support styling the tap highlight color.
if (!('webkitTapHighlightColor' in document.body.style)) {
Expand Down Expand Up @@ -2010,3 +2030,21 @@ class DraggableNgContainerWithAlternateRoot {
class PlainStandaloneDraggable {
@ViewChild(CdkDrag) dragInstance: CdkDrag;
}

@Component({
template: `
<div #dragElement cdkDrag
style="width: 100px; height: 100px; background: red; position: relative">
<ng-container [ngTemplateOutlet]="template"/>
</div>
<ng-template #template>
<div cdkDragHandle class="handle" style="width: 10px; height: 10px; background: green;"></div>
</ng-template>
`,
standalone: true,
imports: [CdkDrag, CdkDragHandle, NgTemplateOutlet],
})
class StandaloneDraggableWithExternalTemplateHandle {
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
}
4 changes: 3 additions & 1 deletion tools/public_api_guard/cdk/drag-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export interface CdkDragExit<T = any, I = T> {
}

// @public
export class CdkDragHandle implements OnDestroy {
export class CdkDragHandle implements AfterViewInit, OnDestroy {
constructor(...args: unknown[]);
get disabled(): boolean;
set disabled(value: boolean);
Expand All @@ -159,6 +159,8 @@ export class CdkDragHandle implements OnDestroy {
// (undocumented)
static ngAcceptInputType_disabled: unknown;
// (undocumented)
ngAfterViewInit(): void;
// (undocumented)
ngOnDestroy(): void;
readonly _stateChanges: Subject<CdkDragHandle>;
// (undocumented)
Expand Down

0 comments on commit 622057a

Please sign in to comment.