Skip to content

Commit b02a72e

Browse files
crisbetoandrewseguin
authored andcommitted
feat(drag-drop): add the ability to disable dragging (#13722)
Adds inputs to `cdkDrag`, `cdkDropList` and `cdkDragHandle` that allows for dragging to be disabled through those specific elements. Fixes #13651.
1 parent cb56df9 commit b02a72e

File tree

9 files changed

+184
-4
lines changed

9 files changed

+184
-4
lines changed

src/cdk/drag-drop/drag-drop.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,11 @@ new container. Depending on whether the predicate returns `true` or `false`, the
158158
be allowed into the new container.
159159

160160
<!-- example(cdk-drag-drop-enter-predicate) -->
161+
162+
### Disable dragging
163+
If you want to disable dragging for a particular drag item, you can do so by setting the
164+
`cdkDragDisabled` input on a `cdkDrag` item. Furthermore, you can disable an entire list
165+
using the `cdkDropListDisabled` input on a `cdkDropList` or a particular handle via
166+
`cdkDragHandleDisabled` on `cdkDragHandle`.
167+
168+
<!-- example(cdk-drag-drop-disabled) -->

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

Lines changed: 10 additions & 1 deletion
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, Inject, Optional} from '@angular/core';
9+
import {Directive, ElementRef, Inject, Optional, Input} from '@angular/core';
10+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
1011
import {CDK_DRAG_PARENT} from './drag-parent';
1112
import {toggleNativeDragInteractions} from './drag-styling';
1213

@@ -21,6 +22,14 @@ export class CdkDragHandle {
2122
/** Closest parent draggable instance. */
2223
_parentDrag: {} | undefined;
2324

25+
/** Whether starting to drag through this handle is disabled. */
26+
@Input('cdkDragHandleDisabled')
27+
get disabled(): boolean { return this._disabled; }
28+
set disabled(value: boolean) {
29+
this._disabled = coerceBooleanProperty(value);
30+
}
31+
private _disabled = false;
32+
2433
constructor(
2534
public element: ElementRef<HTMLElement>,
2635
@Inject(CDK_DRAG_PARENT) @Optional() parentDrag?: any) {

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,18 @@ describe('CdkDrag', () => {
452452
expect(dragElement.style.transform).toBeFalsy();
453453
}));
454454

455+
it('should not be able to drag the element if dragging is disabled', fakeAsync(() => {
456+
const fixture = createComponent(StandaloneDraggable);
457+
fixture.detectChanges();
458+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
459+
460+
fixture.componentInstance.dragInstance.disabled = true;
461+
462+
expect(dragElement.style.transform).toBeFalsy();
463+
dragElementViaMouse(fixture, dragElement, 50, 100);
464+
expect(dragElement.style.transform).toBeFalsy();
465+
}));
466+
455467
it('should stop propagation for the drag sequence start event', fakeAsync(() => {
456468
const fixture = createComponent(StandaloneDraggable);
457469
fixture.detectChanges();
@@ -557,6 +569,32 @@ describe('CdkDrag', () => {
557569
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
558570
}));
559571

572+
it('should not be able to drag the element if the handle is disabled', fakeAsync(() => {
573+
const fixture = createComponent(StandaloneDraggableWithHandle);
574+
fixture.detectChanges();
575+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
576+
const handle = fixture.componentInstance.handleElement.nativeElement;
577+
578+
fixture.componentInstance.handleInstance.disabled = true;
579+
580+
expect(dragElement.style.transform).toBeFalsy();
581+
dragElementViaMouse(fixture, handle, 50, 100);
582+
expect(dragElement.style.transform).toBeFalsy();
583+
}));
584+
585+
it('should not be able to drag using the handle if the element is disabled', fakeAsync(() => {
586+
const fixture = createComponent(StandaloneDraggableWithHandle);
587+
fixture.detectChanges();
588+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
589+
const handle = fixture.componentInstance.handleElement.nativeElement;
590+
591+
fixture.componentInstance.dragInstance.disabled = true;
592+
593+
expect(dragElement.style.transform).toBeFalsy();
594+
dragElementViaMouse(fixture, handle, 50, 100);
595+
expect(dragElement.style.transform).toBeFalsy();
596+
}));
597+
560598
it('should be able to use a handle that was added after init', fakeAsync(() => {
561599
const fixture = createComponent(StandaloneDraggableWithDelayedHandle);
562600

@@ -1698,6 +1736,30 @@ describe('CdkDrag', () => {
16981736
expect(thirdItem.style.transform).toBeFalsy();
16991737
}));
17001738

1739+
it('should not move the item if the list is disabled', fakeAsync(() => {
1740+
const fixture = createComponent(DraggableInDropZone);
1741+
fixture.detectChanges();
1742+
const dragItems = fixture.componentInstance.dragItems;
1743+
1744+
fixture.componentInstance.dropInstance.disabled = true;
1745+
1746+
expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
1747+
.toEqual(['Zero', 'One', 'Two', 'Three']);
1748+
1749+
const firstItem = dragItems.first;
1750+
const thirdItemRect = dragItems.toArray()[2].element.nativeElement.getBoundingClientRect();
1751+
1752+
dragElementViaMouse(fixture, firstItem.element.nativeElement,
1753+
thirdItemRect.right + 1, thirdItemRect.top + 1);
1754+
flush();
1755+
fixture.detectChanges();
1756+
1757+
expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled();
1758+
1759+
expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
1760+
.toEqual(['Zero', 'One', 'Two', 'Three']);
1761+
}));
1762+
17011763
});
17021764

17031765
describe('in a connected drop container', () => {
@@ -2178,6 +2240,8 @@ class StandaloneDraggable {
21782240
class StandaloneDraggableWithHandle {
21792241
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
21802242
@ViewChild('handleElement') handleElement: ElementRef<HTMLElement>;
2243+
@ViewChild(CdkDrag) dragInstance: CdkDrag;
2244+
@ViewChild(CdkDragHandle) handleInstance: CdkDragHandle;
21812245
}
21822246

21832247
@Component({

src/cdk/drag-drop/drag.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
ViewContainerRef,
3030
} from '@angular/core';
3131
import {normalizePassiveListenerOptions} from '@angular/cdk/platform';
32+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
3233
import {Observable, Subject, Subscription, Observer} from 'rxjs';
3334
import {startWith, take} from 'rxjs/operators';
3435
import {DragDropRegistry} from './drag-drop-registry';
@@ -217,6 +218,16 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
217218
*/
218219
@Input('cdkDragRootElement') rootElementSelector: string;
219220

221+
/** Whether starting to drag this element is disabled. */
222+
@Input('cdkDragDisabled')
223+
get disabled(): boolean {
224+
return this._disabled || (this.dropContainer && this.dropContainer.disabled);
225+
}
226+
set disabled(value: boolean) {
227+
this._disabled = coerceBooleanProperty(value);
228+
}
229+
private _disabled = false;
230+
220231
/** Emits when the user starts dragging the item. */
221232
@Output('cdkDragStarted') started: EventEmitter<CdkDragStart> = new EventEmitter<CdkDragStart>();
222233

@@ -351,10 +362,10 @@ export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
351362
return !!target && (target === element || element.contains(target as HTMLElement));
352363
});
353364

354-
if (targetHandle) {
365+
if (targetHandle && !targetHandle.disabled && !this.disabled) {
355366
this._initializeDragSequence(targetHandle.element.nativeElement, event);
356367
}
357-
} else {
368+
} else if (!this.disabled) {
358369
this._initializeDragSequence(this._rootElement, event);
359370
}
360371
}

src/cdk/drag-drop/drop-list-container.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export interface CdkDropListContainer<T = any> {
2626
/** Locks the position of the draggable elements inside the container along the specified axis. */
2727
lockAxis: 'x' | 'y';
2828

29+
/** Whether starting a dragging sequence from this container is disabled. */
30+
disabled: boolean;
31+
2932
/** Starts dragging an item. */
3033
start(): void;
3134

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {coerceArray} from '@angular/cdk/coercion';
9+
import {coerceArray, coerceBooleanProperty} from '@angular/cdk/coercion';
1010
import {
1111
ContentChildren,
1212
ElementRef,
@@ -116,6 +116,14 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
116116
/** Locks the position of the draggable elements inside the container along the specified axis. */
117117
@Input('cdkDropListLockAxis') lockAxis: 'x' | 'y';
118118

119+
/** Whether starting a dragging sequence from this container is disabled. */
120+
@Input('cdkDropListDisabled')
121+
get disabled(): boolean { return this._disabled; }
122+
set disabled(value: boolean) {
123+
this._disabled = coerceBooleanProperty(value);
124+
}
125+
private _disabled = false;
126+
119127
/**
120128
* Function that is used to determine whether an item
121129
* is allowed to be moved into a drop container.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
.example-list {
2+
width: 500px;
3+
max-width: 100%;
4+
border: solid 1px #ccc;
5+
min-height: 60px;
6+
display: block;
7+
background: white;
8+
border-radius: 4px;
9+
overflow: hidden;
10+
}
11+
12+
.example-box {
13+
padding: 20px 10px;
14+
border-bottom: solid 1px #ccc;
15+
color: rgba(0, 0, 0, 0.87);
16+
display: flex;
17+
flex-direction: row;
18+
align-items: center;
19+
justify-content: space-between;
20+
box-sizing: border-box;
21+
cursor: move;
22+
background: white;
23+
font-size: 14px;
24+
}
25+
26+
.cdk-drag-preview {
27+
box-sizing: border-box;
28+
border-radius: 4px;
29+
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
30+
0 8px 10px 1px rgba(0, 0, 0, 0.14),
31+
0 3px 14px 2px rgba(0, 0, 0, 0.12);
32+
}
33+
34+
.cdk-drag-placeholder {
35+
opacity: 0;
36+
}
37+
38+
.cdk-drag-animating {
39+
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
40+
}
41+
42+
.example-box:last-child {
43+
border: none;
44+
}
45+
46+
.example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) {
47+
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
48+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<div cdkDropList class="example-list" (cdkDropListDropped)="drop($event)">
2+
<div
3+
class="example-box"
4+
*ngFor="let item of items"
5+
cdkDrag
6+
[cdkDragDisabled]="item.disabled">{{item.value}}</div>
7+
</div>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {Component} from '@angular/core';
2+
import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop';
3+
4+
/**
5+
* @title Drag&Drop disabled
6+
*/
7+
@Component({
8+
selector: 'cdk-drag-drop-disabled-example',
9+
templateUrl: 'cdk-drag-drop-disabled-example.html',
10+
styleUrls: ['cdk-drag-drop-disabled-example.css'],
11+
})
12+
export class CdkDragDropDisabledExample {
13+
items = [
14+
{value: 'I can be dragged', disabled: false},
15+
{value: 'I cannot be dragged', disabled: true},
16+
{value: 'I can also be dragged', disabled: false}
17+
];
18+
19+
drop(event: CdkDragDrop<string[]>) {
20+
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
21+
}
22+
}

0 commit comments

Comments
 (0)