Skip to content

Commit 07e9b8d

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 04234f0 commit 07e9b8d

File tree

4 files changed

+88
-7
lines changed

4 files changed

+88
-7
lines changed

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

Lines changed: 11 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

1112
/** Handle that can be used to drag and CdkDrag instance. */
1213
@Directive({
@@ -16,5 +17,13 @@ import {Directive, ElementRef} from '@angular/core';
1617
}
1718
})
1819
export class CdkDragHandle {
19-
constructor(public element: ElementRef<HTMLElement>) {}
20+
/** Closest parent draggable instance. */
21+
_parentDrag: {} | undefined;
22+
23+
constructor(
24+
public element: ElementRef<HTMLElement>,
25+
@Inject(CDK_DRAG_PARENT) @Optional() parentDrag?: any) {
26+
27+
this._parentDrag = parentDrag;
28+
}
2029
}

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
}).compileComponents();
3939

@@ -392,6 +392,23 @@ describe('CdkDrag', () => {
392392
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
393393
}));
394394

395+
it('should be able to drag with a handle that is not a direct descendant', fakeAsync(() => {
396+
const fixture = createComponent(StandaloneDraggableWithIndirectHandle);
397+
fixture.detectChanges();
398+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
399+
const handle = fixture.componentInstance.handleElement.nativeElement;
400+
401+
expect(dragElement.style.transform).toBeFalsy();
402+
dragElementViaMouse(fixture, dragElement, 50, 100);
403+
404+
expect(dragElement.style.transform)
405+
.toBeFalsy('Expected not to be able to drag the element by itself.');
406+
407+
dragElementViaMouse(fixture, handle, 50, 100);
408+
expect(dragElement.style.transform)
409+
.toBe('translate3d(50px, 100px, 0px)', 'Expected to drag the element by its handle.');
410+
}));
411+
395412
});
396413

397414
describe('in a drop container', () => {
@@ -1640,6 +1657,26 @@ class StandaloneDraggableWithDelayedHandle {
16401657
showHandle = false;
16411658
}
16421659

1660+
@Component({
1661+
template: `
1662+
<div #dragElement cdkDrag
1663+
style="width: 100px; height: 100px; background: red; position: relative">
1664+
1665+
<passthrough-component>
1666+
<div
1667+
#handleElement
1668+
cdkDragHandle
1669+
style="width: 10px; height: 10px; background: green;"></div>
1670+
</passthrough-component>
1671+
</div>
1672+
`
1673+
})
1674+
class StandaloneDraggableWithIndirectHandle {
1675+
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
1676+
@ViewChild('handleElement') handleElement: ElementRef<HTMLElement>;
1677+
}
1678+
1679+
16431680
@Component({
16441681
encapsulation: ViewEncapsulation.None,
16451682
styles: [`
@@ -1868,6 +1905,16 @@ class ConnectedDropZonesWithSingleItems {
18681905
droppedSpy = jasmine.createSpy('dropped spy');
18691906
}
18701907

1908+
/**
1909+
* Component that passes through whatever content is projected into it.
1910+
* Used to test having drag elements being projected into a component.
1911+
*/
1912+
@Component({
1913+
selector: 'passthrough-component',
1914+
template: '<ng-content></ng-content>'
1915+
})
1916+
class PassthroughComponent {}
1917+
18711918
/**
18721919
* Drags an element to a position on the page using the mouse.
18731920
* @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
@@ -42,6 +42,7 @@ import {CdkDragHandle} from './drag-handle';
4242
import {CdkDragPlaceholder} from './drag-placeholder';
4343
import {CdkDragPreview} from './drag-preview';
4444
import {CDK_DROP_CONTAINER, CdkDropContainer} from './drop-container';
45+
import {CDK_DRAG_PARENT} from './drag-parent';
4546

4647

4748
// TODO(crisbeto): add auto-scrolling functionality.
@@ -61,7 +62,11 @@ const POINTER_DIRECTION_CHANGE_THRESHOLD = 5;
6162
host: {
6263
'class': 'cdk-drag',
6364
'[class.cdk-drag-dragging]': '_isDragging()',
64-
}
65+
},
66+
providers: [{
67+
provide: CDK_DRAG_PARENT,
68+
useExisting: CdkDrag
69+
}]
6570
})
6671
export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
6772
private _document: Document;
@@ -130,7 +135,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
130135
private _rootElement: HTMLElement;
131136

132137
/** Elements that can be used to drag the draggable item. */
133-
@ContentChildren(CdkDragHandle) _handles: QueryList<CdkDragHandle>;
138+
@ContentChildren(CdkDragHandle, {descendants: true}) _handles: QueryList<CdkDragHandle>;
134139

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

251256
/** Starts the dragging sequence. */
252257
_startDragging = (event: MouseEvent | TouchEvent) => {
258+
// Skip handles inside descendant `CdkDrag` instances.
259+
const handles = this._handles.filter(handle => handle._parentDrag === this);
260+
253261
// Delegate the event based on whether it started from a handle or the element itself.
254-
if (this._handles.length) {
255-
const targetHandle = this._handles.find(handle => {
262+
if (handles.length) {
263+
const targetHandle = handles.find(handle => {
256264
const element = handle.element.nativeElement;
257265
const target = event.target;
258266
return !!target && (target === element || element.contains(target as HTMLElement));

0 commit comments

Comments
 (0)