Skip to content

Commit c38ebb6

Browse files
crisbetojosephperrott
authored andcommitted
fix(drag-drop): not picking up handle that isn't a direct descendant (#13360)
1 parent fb8496d commit c38ebb6

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,
@@ -466,6 +466,23 @@ describe('CdkDrag', () => {
466466
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
467467
}));
468468

469+
it('should be able to drag with a handle that is not a direct descendant', fakeAsync(() => {
470+
const fixture = createComponent(StandaloneDraggableWithIndirectHandle);
471+
fixture.detectChanges();
472+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
473+
const handle = fixture.componentInstance.handleElement.nativeElement;
474+
475+
expect(dragElement.style.transform).toBeFalsy();
476+
dragElementViaMouse(fixture, dragElement, 50, 100);
477+
478+
expect(dragElement.style.transform)
479+
.toBeFalsy('Expected not to be able to drag the element by itself.');
480+
481+
dragElementViaMouse(fixture, handle, 50, 100);
482+
expect(dragElement.style.transform)
483+
.toBe('translate3d(50px, 100px, 0px)', 'Expected to drag the element by its handle.');
484+
}));
485+
469486
});
470487

471488
describe('in a drop container', () => {
@@ -1746,6 +1763,26 @@ class StandaloneDraggableWithDelayedHandle {
17461763
showHandle = false;
17471764
}
17481765

1766+
@Component({
1767+
template: `
1768+
<div #dragElement cdkDrag
1769+
style="width: 100px; height: 100px; background: red; position: relative">
1770+
1771+
<passthrough-component>
1772+
<div
1773+
#handleElement
1774+
cdkDragHandle
1775+
style="width: 10px; height: 10px; background: green;"></div>
1776+
</passthrough-component>
1777+
</div>
1778+
`
1779+
})
1780+
class StandaloneDraggableWithIndirectHandle {
1781+
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
1782+
@ViewChild('handleElement') handleElement: ElementRef<HTMLElement>;
1783+
}
1784+
1785+
17491786
@Component({
17501787
encapsulation: ViewEncapsulation.None,
17511788
styles: [`
@@ -1986,6 +2023,16 @@ class ConnectedDropZonesWithSingleItems {
19862023
droppedSpy = jasmine.createSpy('dropped spy');
19872024
}
19882025

2026+
/**
2027+
* Component that passes through whatever content is projected into it.
2028+
* Used to test having drag elements being projected into a component.
2029+
*/
2030+
@Component({
2031+
selector: 'passthrough-component',
2032+
template: '<ng-content></ng-content>'
2033+
})
2034+
class PassthroughComponent {}
2035+
19892036
/**
19902037
* Drags an element to a position on the page using the mouse.
19912038
* @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
@@ -46,6 +46,7 @@ import {CdkDragPreview} from './drag-preview';
4646
import {CDK_DROP_LIST_CONTAINER, CdkDropListContainer} from './drop-list-container';
4747
import {getTransformTransitionDurationInMs} from './transition-duration';
4848
import {extendStyles, toggleNativeDragInteractions} from './drag-styling';
49+
import {CDK_DRAG_PARENT} from './drag-parent';
4950

5051

5152
// TODO(crisbeto): add auto-scrolling functionality.
@@ -89,7 +90,11 @@ const passiveEventListenerOptions = supportsPassiveEventListeners() ?
8990
host: {
9091
'class': 'cdk-drag',
9192
'[class.cdk-drag-dragging]': '_hasStartedDragging && _isDragging()',
92-
}
93+
},
94+
providers: [{
95+
provide: CDK_DRAG_PARENT,
96+
useExisting: CdkDrag
97+
}]
9398
})
9499
export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
95100
private _document: Document;
@@ -172,7 +177,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
172177
private _pointerUpSubscription = Subscription.EMPTY;
173178

174179
/** Elements that can be used to drag the draggable item. */
175-
@ContentChildren(CdkDragHandle) _handles: QueryList<CdkDragHandle>;
180+
@ContentChildren(CdkDragHandle, {descendants: true}) _handles: QueryList<CdkDragHandle>;
176181

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

299304
/** Handler for the `mousedown`/`touchstart` events. */
300305
_pointerDown = (event: MouseEvent | TouchEvent) => {
306+
// Skip handles inside descendant `CdkDrag` instances.
307+
const handles = this._handles.filter(handle => handle._parentDrag === this);
308+
301309
// Delegate the event based on whether it started from a handle or the element itself.
302-
if (this._handles.length) {
303-
const targetHandle = this._handles.find(handle => {
310+
if (handles.length) {
311+
const targetHandle = handles.find(handle => {
304312
const element = handle.element.nativeElement;
305313
const target = event.target;
306314
return !!target && (target === element || element.contains(target as HTMLElement));

0 commit comments

Comments
 (0)