Skip to content

Commit 6074ff4

Browse files
committed
fix(drag-drop): not picking up handle that isn't a direct descendant
Fixes not being able to have a drag handle that isn't a direct descendant of `CdkDrag`. Fixes #13335.
1 parent 2e4a511 commit 6074ff4

File tree

4 files changed

+87
-7
lines changed

4 files changed

+87
-7
lines changed

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Directive, ElementRef} from '@angular/core';
9+
import {Directive, ElementRef, Inject, Optional} from '@angular/core';
10+
import {CDK_DRAG_PARENT} from './drag-parent';
1011
import {toggleNativeDragInteractions} from './drag-styling';
1112

1213
/** Handle that can be used to drag and CdkDrag instance. */
@@ -17,7 +18,14 @@ import {toggleNativeDragInteractions} from './drag-styling';
1718
}
1819
})
1920
export class CdkDragHandle {
20-
constructor(public element: ElementRef<HTMLElement>) {
21+
/** Closest parent draggable instance. */
22+
_parentDrag: {} | undefined;
23+
24+
constructor(
25+
public element: ElementRef<HTMLElement>,
26+
@Inject(CDK_DRAG_PARENT) @Optional() parentDrag?: any) {
27+
28+
this._parentDrag = parentDrag;
2129
toggleNativeDragInteractions(element.nativeElement, false);
2230
}
2331
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {InjectionToken} from '@angular/core';
10+
11+
/**
12+
* Injection token that can be used for a `CdkDrag` to provide itself as a parent to the
13+
* drag-specific child directive (`CdkDragHandle`, `CdkDragPreview` etc.). Used primarily
14+
* to avoid circular imports.
15+
* @docs-private
16+
*/
17+
export const CDK_DRAG_PARENT = new InjectionToken<{}>('CDK_DRAG_PARENT');

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

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('CdkDrag', () => {
3333
ComponentFixture<T> {
3434
TestBed.configureTestingModule({
3535
imports: [DragDropModule],
36-
declarations: [componentType],
36+
declarations: [componentType, PassthroughComponent],
3737
providers: [
3838
{
3939
provide: CDK_DRAG_CONFIG,
@@ -442,6 +442,23 @@ describe('CdkDrag', () => {
442442
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
443443
}));
444444

445+
it('should be able to drag with a handle that is not a direct descendant', fakeAsync(() => {
446+
const fixture = createComponent(StandaloneDraggableWithIndirectHandle);
447+
fixture.detectChanges();
448+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
449+
const handle = fixture.componentInstance.handleElement.nativeElement;
450+
451+
expect(dragElement.style.transform).toBeFalsy();
452+
dragElementViaMouse(fixture, dragElement, 50, 100);
453+
454+
expect(dragElement.style.transform)
455+
.toBeFalsy('Expected not to be able to drag the element by itself.');
456+
457+
dragElementViaMouse(fixture, handle, 50, 100);
458+
expect(dragElement.style.transform)
459+
.toBe('translate3d(50px, 100px, 0px)', 'Expected to drag the element by its handle.');
460+
}));
461+
445462
});
446463

447464
describe('in a drop container', () => {
@@ -1700,6 +1717,26 @@ class StandaloneDraggableWithDelayedHandle {
17001717
showHandle = false;
17011718
}
17021719

1720+
@Component({
1721+
template: `
1722+
<div #dragElement cdkDrag
1723+
style="width: 100px; height: 100px; background: red; position: relative">
1724+
1725+
<passthrough-component>
1726+
<div
1727+
#handleElement
1728+
cdkDragHandle
1729+
style="width: 10px; height: 10px; background: green;"></div>
1730+
</passthrough-component>
1731+
</div>
1732+
`
1733+
})
1734+
class StandaloneDraggableWithIndirectHandle {
1735+
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
1736+
@ViewChild('handleElement') handleElement: ElementRef<HTMLElement>;
1737+
}
1738+
1739+
17031740
@Component({
17041741
encapsulation: ViewEncapsulation.None,
17051742
styles: [`
@@ -1940,6 +1977,16 @@ class ConnectedDropZonesWithSingleItems {
19401977
droppedSpy = jasmine.createSpy('dropped spy');
19411978
}
19421979

1980+
/**
1981+
* Component that passes through whatever content is projected into it.
1982+
* Used to test having drag elements being projected into a component.
1983+
*/
1984+
@Component({
1985+
selector: 'passthrough-component',
1986+
template: '<ng-content></ng-content>'
1987+
})
1988+
class PassthroughComponent {}
1989+
19431990
/**
19441991
* Drags an element to a position on the page using the mouse.
19451992
* @param fixture Fixture on which to run change detection.

src/cdk/drag-drop/drag.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {CdkDragPreview} from './drag-preview';
4545
import {CDK_DROP_CONTAINER, CdkDropContainer} from './drop-container';
4646
import {getTransformTransitionDurationInMs} from './transition-duration';
4747
import {extendStyles, toggleNativeDragInteractions} from './drag-styling';
48+
import {CDK_DRAG_PARENT} from './drag-parent';
4849

4950

5051
// TODO(crisbeto): add auto-scrolling functionality.
@@ -84,7 +85,11 @@ export function CDK_DRAG_CONFIG_FACTORY(): CdkDragConfig {
8485
host: {
8586
'class': 'cdk-drag',
8687
'[class.cdk-drag-dragging]': '_hasStartedDragging && _isDragging()',
87-
}
88+
},
89+
providers: [{
90+
provide: CDK_DRAG_PARENT,
91+
useExisting: CdkDrag
92+
}]
8893
})
8994
export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
9095
private _document: Document;
@@ -164,7 +169,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
164169
private _pointerUpSubscription = Subscription.EMPTY;
165170

166171
/** Elements that can be used to drag the draggable item. */
167-
@ContentChildren(CdkDragHandle) _handles: QueryList<CdkDragHandle>;
172+
@ContentChildren(CdkDragHandle, {descendants: true}) _handles: QueryList<CdkDragHandle>;
168173

169174
/** Element that will be used as a template to create the draggable item's preview. */
170175
@ContentChild(CdkDragPreview) _previewTemplate: CdkDragPreview;
@@ -287,9 +292,12 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
287292

288293
/** Handler for the `mousedown`/`touchstart` events. */
289294
_pointerDown = (event: MouseEvent | TouchEvent) => {
295+
// Skip handles inside descendant `CdkDrag` instances.
296+
const handles = this._handles.filter(handle => handle._parentDrag === this);
297+
290298
// Delegate the event based on whether it started from a handle or the element itself.
291-
if (this._handles.length) {
292-
const targetHandle = this._handles.find(handle => {
299+
if (handles.length) {
300+
const targetHandle = handles.find(handle => {
293301
const element = handle.element.nativeElement;
294302
const target = event.target;
295303
return !!target && (target === element || element.contains(target as HTMLElement));

0 commit comments

Comments
 (0)