fix(drag-drop): not picking up handle that isn't a direct descendant (#13360)

This commit is contained in:
Kristiyan Kostadinov
2018-10-19 23:03:36 +02:00
committed by Joey Perrott
parent fb8496dc45
commit c38ebb63cb
4 changed files with 87 additions and 7 deletions
+10 -2
View File
@@ -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<HTMLElement>) {
/** Closest parent draggable instance. */
_parentDrag: {} | undefined;
constructor(
public element: ElementRef<HTMLElement>,
@Inject(CDK_DRAG_PARENT) @Optional() parentDrag?: any) {
this._parentDrag = parentDrag;
toggleNativeDragInteractions(element.nativeElement, false);
}
}
+17
View File
@@ -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');
+48 -1
View File
@@ -33,7 +33,7 @@ describe('CdkDrag', () => {
ComponentFixture<T> {
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: `
<div #dragElement cdkDrag
style="width: 100px; height: 100px; background: red; position: relative">
<passthrough-component>
<div
#handleElement
cdkDragHandle
style="width: 10px; height: 10px; background: green;"></div>
</passthrough-component>
</div>
`
})
class StandaloneDraggableWithIndirectHandle {
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
@ViewChild('handleElement') handleElement: ElementRef<HTMLElement>;
}
@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: '<ng-content></ng-content>'
})
class PassthroughComponent {}
/**
* Drags an element to a position on the page using the mouse.
* @param fixture Fixture on which to run change detection.
+12 -4
View File
@@ -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<T = any> implements AfterViewInit, OnDestroy {
private _document: Document;
@@ -172,7 +177,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
private _pointerUpSubscription = Subscription.EMPTY;
/** Elements that can be used to drag the draggable item. */
@ContentChildren(CdkDragHandle) _handles: QueryList<CdkDragHandle>;
@ContentChildren(CdkDragHandle, {descendants: true}) _handles: QueryList<CdkDragHandle>;
/** 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<T = any> 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));