From 622057a146df8acc8d77192dc4c2a8102dea7b56 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 12 Dec 2024 09:45:04 +0100 Subject: [PATCH] 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 a141c22e99467174225f74b6ccac730df92c46c8) --- src/cdk/drag-drop/directives/drag-handle.ts | 20 +++++++++- .../directives/standalone-drag.spec.ts | 38 +++++++++++++++++++ tools/public_api_guard/cdk/drag-drop.md | 4 +- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/cdk/drag-drop/directives/drag-handle.ts b/src/cdk/drag-drop/directives/drag-handle.ts index 9dd1bd514181..01d11aa430c8 100644 --- a/src/cdk/drag-drop/directives/drag-handle.ts +++ b/src/cdk/drag-drop/directives/drag-handle.ts @@ -7,6 +7,7 @@ */ import { + AfterViewInit, Directive, ElementRef, InjectionToken, @@ -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 @@ -35,10 +37,11 @@ export const CDK_DRAG_HANDLE = new InjectionToken('CdkDragHandle' }, providers: [{provide: CDK_DRAG_HANDLE, useExisting: CdkDragHandle}], }) -export class CdkDragHandle implements OnDestroy { +export class CdkDragHandle implements AfterViewInit, OnDestroy { element = inject>(ElementRef); private _parentDrag = inject(CDK_DRAG_PARENT, {optional: true, skipSelf: true}); + private _dragDropRegistry = inject(DragDropRegistry); /** Emits when the state of the handle has changed. */ readonly _stateChanges = new Subject(); @@ -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(); diff --git a/src/cdk/drag-drop/directives/standalone-drag.spec.ts b/src/cdk/drag-drop/directives/standalone-drag.spec.ts index 775f9e69ef91..c4449abe204e 100644 --- a/src/cdk/drag-drop/directives/standalone-drag.spec.ts +++ b/src/cdk/drag-drop/directives/standalone-drag.spec.ts @@ -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, @@ -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)) { @@ -2010,3 +2030,21 @@ class DraggableNgContainerWithAlternateRoot { class PlainStandaloneDraggable { @ViewChild(CdkDrag) dragInstance: CdkDrag; } + +@Component({ + template: ` +
+ +
+ + +
+
+ `, + standalone: true, + imports: [CdkDrag, CdkDragHandle, NgTemplateOutlet], +}) +class StandaloneDraggableWithExternalTemplateHandle { + @ViewChild('dragElement') dragElement: ElementRef; +} diff --git a/tools/public_api_guard/cdk/drag-drop.md b/tools/public_api_guard/cdk/drag-drop.md index 515402b448e0..0624dfeb708b 100644 --- a/tools/public_api_guard/cdk/drag-drop.md +++ b/tools/public_api_guard/cdk/drag-drop.md @@ -150,7 +150,7 @@ export interface CdkDragExit { } // @public -export class CdkDragHandle implements OnDestroy { +export class CdkDragHandle implements AfterViewInit, OnDestroy { constructor(...args: unknown[]); get disabled(): boolean; set disabled(value: boolean); @@ -159,6 +159,8 @@ export class CdkDragHandle implements OnDestroy { // (undocumented) static ngAcceptInputType_disabled: unknown; // (undocumented) + ngAfterViewInit(): void; + // (undocumented) ngOnDestroy(): void; readonly _stateChanges: Subject; // (undocumented)