Skip to content

Commit ca8ce11

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 4b15b78 commit ca8ce11

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
{
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: [`
@@ -1928,6 +1965,16 @@ class ConnectedDropZonesWithSingleItems {
19281965
droppedSpy = jasmine.createSpy('dropped spy');
19291966
}
19301967

1968+
/**
1969+
* Component that passes through whatever content is projected into it.
1970+
* Used to test having drag elements being projected into a component.
1971+
*/
1972+
@Component({
1973+
selector: 'passthrough-component',
1974+
template: '<ng-content></ng-content>'
1975+
})
1976+
class PassthroughComponent {}
1977+
19311978
/**
19321979
* Drags an element to a position on the page using the mouse.
19331980
* @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
@@ -44,6 +44,7 @@ import {CdkDragPlaceholder} from './drag-placeholder';
4444
import {CdkDragPreview} from './drag-preview';
4545
import {CDK_DROP_CONTAINER, CdkDropContainer} from './drop-container';
4646
import {getTransformTransitionDurationInMs} from './transition-duration';
47+
import {CDK_DRAG_PARENT} from './drag-parent';
4748

4849

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

165170
/** Elements that can be used to drag the draggable item. */
166-
@ContentChildren(CdkDragHandle) _handles: QueryList<CdkDragHandle>;
171+
@ContentChildren(CdkDragHandle, {descendants: true}) _handles: QueryList<CdkDragHandle>;
167172

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

286291
/** Handler for the `mousedown`/`touchstart` events. */
287292
_pointerDown = (event: MouseEvent | TouchEvent) => {
293+
// Skip handles inside descendant `CdkDrag` instances.
294+
const handles = this._handles.filter(handle => handle._parentDrag === this);
295+
288296
// Delegate the event based on whether it started from a handle or the element itself.
289-
if (this._handles.length) {
290-
const targetHandle = this._handles.find(handle => {
297+
if (handles.length) {
298+
const targetHandle = handles.find(handle => {
291299
const element = handle.element.nativeElement;
292300
const target = event.target;
293301
return !!target && (target === element || element.contains(target as HTMLElement));

0 commit comments

Comments
 (0)