From c38ebb63cb2491733af53d816439d3ae4072d419 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 19 Oct 2018 23:03:36 +0200 Subject: [PATCH] fix(drag-drop): not picking up handle that isn't a direct descendant (#13360) --- src/cdk/drag-drop/drag-handle.ts | 12 ++++++-- src/cdk/drag-drop/drag-parent.ts | 17 +++++++++++ src/cdk/drag-drop/drag.spec.ts | 49 +++++++++++++++++++++++++++++++- src/cdk/drag-drop/drag.ts | 16 ++++++++--- 4 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 src/cdk/drag-drop/drag-parent.ts diff --git a/src/cdk/drag-drop/drag-handle.ts b/src/cdk/drag-drop/drag-handle.ts index a1fac346301..cca7bee3836 100644 --- a/src/cdk/drag-drop/drag-handle.ts +++ b/src/cdk/drag-drop/drag-handle.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef} from '@angular/core'; +import {Directive, ElementRef, Inject, Optional} from '@angular/core'; +import {CDK_DRAG_PARENT} from './drag-parent'; import {toggleNativeDragInteractions} from './drag-styling'; /** Handle that can be used to drag and CdkDrag instance. */ @@ -17,7 +18,14 @@ import {toggleNativeDragInteractions} from './drag-styling'; } }) export class CdkDragHandle { - constructor(public element: ElementRef) { + /** Closest parent draggable instance. */ + _parentDrag: {} | undefined; + + constructor( + public element: ElementRef, + @Inject(CDK_DRAG_PARENT) @Optional() parentDrag?: any) { + + this._parentDrag = parentDrag; toggleNativeDragInteractions(element.nativeElement, false); } } diff --git a/src/cdk/drag-drop/drag-parent.ts b/src/cdk/drag-drop/drag-parent.ts new file mode 100644 index 00000000000..5df90660c7f --- /dev/null +++ b/src/cdk/drag-drop/drag-parent.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {InjectionToken} from '@angular/core'; + +/** + * Injection token that can be used for a `CdkDrag` to provide itself as a parent to the + * drag-specific child directive (`CdkDragHandle`, `CdkDragPreview` etc.). Used primarily + * to avoid circular imports. + * @docs-private + */ +export const CDK_DRAG_PARENT = new InjectionToken<{}>('CDK_DRAG_PARENT'); diff --git a/src/cdk/drag-drop/drag.spec.ts b/src/cdk/drag-drop/drag.spec.ts index 320082448bb..76eed1e128e 100644 --- a/src/cdk/drag-drop/drag.spec.ts +++ b/src/cdk/drag-drop/drag.spec.ts @@ -33,7 +33,7 @@ describe('CdkDrag', () => { ComponentFixture { TestBed.configureTestingModule({ imports: [DragDropModule], - declarations: [componentType], + declarations: [componentType, PassthroughComponent], providers: [ { provide: CDK_DRAG_CONFIG, @@ -466,6 +466,23 @@ describe('CdkDrag', () => { expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); })); + it('should be able to drag with a handle that is not a direct descendant', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggableWithIndirectHandle); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const handle = fixture.componentInstance.handleElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + + expect(dragElement.style.transform) + .toBeFalsy('Expected not to be able to drag the element by itself.'); + + dragElementViaMouse(fixture, handle, 50, 100); + expect(dragElement.style.transform) + .toBe('translate3d(50px, 100px, 0px)', 'Expected to drag the element by its handle.'); + })); + }); describe('in a drop container', () => { @@ -1746,6 +1763,26 @@ class StandaloneDraggableWithDelayedHandle { showHandle = false; } +@Component({ + template: ` +
+ + +
+
+
+ ` +}) +class StandaloneDraggableWithIndirectHandle { + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChild('handleElement') handleElement: ElementRef; +} + + @Component({ encapsulation: ViewEncapsulation.None, styles: [` @@ -1986,6 +2023,16 @@ class ConnectedDropZonesWithSingleItems { droppedSpy = jasmine.createSpy('dropped spy'); } +/** + * Component that passes through whatever content is projected into it. + * Used to test having drag elements being projected into a component. + */ +@Component({ + selector: 'passthrough-component', + template: '' +}) +class PassthroughComponent {} + /** * Drags an element to a position on the page using the mouse. * @param fixture Fixture on which to run change detection. diff --git a/src/cdk/drag-drop/drag.ts b/src/cdk/drag-drop/drag.ts index 36d2d5c9590..f26e2e004b5 100644 --- a/src/cdk/drag-drop/drag.ts +++ b/src/cdk/drag-drop/drag.ts @@ -46,6 +46,7 @@ import {CdkDragPreview} from './drag-preview'; import {CDK_DROP_LIST_CONTAINER, CdkDropListContainer} from './drop-list-container'; import {getTransformTransitionDurationInMs} from './transition-duration'; import {extendStyles, toggleNativeDragInteractions} from './drag-styling'; +import {CDK_DRAG_PARENT} from './drag-parent'; // TODO(crisbeto): add auto-scrolling functionality. @@ -89,7 +90,11 @@ const passiveEventListenerOptions = supportsPassiveEventListeners() ? host: { 'class': 'cdk-drag', '[class.cdk-drag-dragging]': '_hasStartedDragging && _isDragging()', - } + }, + providers: [{ + provide: CDK_DRAG_PARENT, + useExisting: CdkDrag + }] }) export class CdkDrag implements AfterViewInit, OnDestroy { private _document: Document; @@ -172,7 +177,7 @@ export class CdkDrag implements AfterViewInit, OnDestroy { private _pointerUpSubscription = Subscription.EMPTY; /** Elements that can be used to drag the draggable item. */ - @ContentChildren(CdkDragHandle) _handles: QueryList; + @ContentChildren(CdkDragHandle, {descendants: true}) _handles: QueryList; /** Element that will be used as a template to create the draggable item's preview. */ @ContentChild(CdkDragPreview) _previewTemplate: CdkDragPreview; @@ -298,9 +303,12 @@ export class CdkDrag implements AfterViewInit, OnDestroy { /** Handler for the `mousedown`/`touchstart` events. */ _pointerDown = (event: MouseEvent | TouchEvent) => { + // Skip handles inside descendant `CdkDrag` instances. + const handles = this._handles.filter(handle => handle._parentDrag === this); + // Delegate the event based on whether it started from a handle or the element itself. - if (this._handles.length) { - const targetHandle = this._handles.find(handle => { + if (handles.length) { + const targetHandle = handles.find(handle => { const element = handle.element.nativeElement; const target = event.target; return !!target && (target === element || element.contains(target as HTMLElement));