Skip to content

Commit 4d430bd

Browse files
crisbetommalerba
authored andcommitted
feat(drag-drop): add the ability to lock dragging along an axis (#12604)
Adds inputs that allow consumers to lock dragging of a particular drag item, or all items in a drag container, along an axis.
1 parent 3a84259 commit 4d430bd

File tree

6 files changed

+137
-8
lines changed

6 files changed

+137
-8
lines changed

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,38 @@ describe('CdkDrag', () => {
246246
subscription.unsubscribe();
247247
});
248248

249+
it('should be able to lock dragging along the x axis', fakeAsync(() => {
250+
const fixture = createComponent(StandaloneDraggable);
251+
fixture.detectChanges();
252+
fixture.componentInstance.dragInstance.lockAxis = 'x';
253+
254+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
255+
256+
expect(dragElement.style.transform).toBeFalsy();
257+
258+
dragElementViaMouse(fixture, dragElement, 50, 100);
259+
expect(dragElement.style.transform).toBe('translate3d(50px, 0px, 0px)');
260+
261+
dragElementViaMouse(fixture, dragElement, 100, 200);
262+
expect(dragElement.style.transform).toBe('translate3d(150px, 0px, 0px)');
263+
}));
264+
265+
it('should be able to lock dragging along the y axis', fakeAsync(() => {
266+
const fixture = createComponent(StandaloneDraggable);
267+
fixture.detectChanges();
268+
fixture.componentInstance.dragInstance.lockAxis = 'y';
269+
270+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
271+
272+
expect(dragElement.style.transform).toBeFalsy();
273+
274+
dragElementViaMouse(fixture, dragElement, 50, 100);
275+
expect(dragElement.style.transform).toBe('translate3d(0px, 100px, 0px)');
276+
277+
dragElementViaMouse(fixture, dragElement, 100, 200);
278+
expect(dragElement.style.transform).toBe('translate3d(0px, 300px, 0px)');
279+
}));
280+
249281
});
250282

251283
describe('draggable with a handle', () => {
@@ -690,6 +722,65 @@ describe('CdkDrag', () => {
690722
expect(preview.style.transform).toBe('translate3d(50px, 50px, 0px)');
691723
}));
692724

725+
it('should lock position inside a drop container along the x axis', fakeAsync(() => {
726+
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
727+
fixture.detectChanges();
728+
729+
const item = fixture.componentInstance.dragItems.toArray()[1];
730+
const element = item.element.nativeElement;
731+
732+
item.lockAxis = 'x';
733+
734+
dispatchMouseEvent(element, 'mousedown', 50, 50);
735+
fixture.detectChanges();
736+
737+
dispatchMouseEvent(element, 'mousemove', 100, 100);
738+
fixture.detectChanges();
739+
740+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
741+
742+
expect(preview.style.transform).toBe('translate3d(100px, 50px, 0px)');
743+
}));
744+
745+
it('should lock position inside a drop container along the y axis', fakeAsync(() => {
746+
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
747+
fixture.detectChanges();
748+
749+
const item = fixture.componentInstance.dragItems.toArray()[1];
750+
const element = item.element.nativeElement;
751+
752+
item.lockAxis = 'y';
753+
754+
dispatchMouseEvent(element, 'mousedown', 50, 50);
755+
fixture.detectChanges();
756+
757+
dispatchMouseEvent(element, 'mousemove', 100, 100);
758+
fixture.detectChanges();
759+
760+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
761+
762+
expect(preview.style.transform).toBe('translate3d(50px, 100px, 0px)');
763+
}));
764+
765+
it('should inherit the position locking from the drop container', fakeAsync(() => {
766+
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
767+
fixture.detectChanges();
768+
769+
const element = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
770+
771+
fixture.componentInstance.dropInstance.lockAxis = 'x';
772+
773+
dispatchMouseEvent(element, 'mousedown', 50, 50);
774+
fixture.detectChanges();
775+
776+
dispatchMouseEvent(element, 'mousemove', 100, 100);
777+
fixture.detectChanges();
778+
779+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
780+
781+
expect(preview.style.transform).toBe('translate3d(100px, 50px, 0px)');
782+
}));
783+
693784
it('should be able to customize the placeholder', fakeAsync(() => {
694785
const fixture = createComponent(DraggableInDropZoneWithCustomPlaceholder);
695786
fixture.detectChanges();
@@ -1103,6 +1194,7 @@ export class DraggableInHorizontalDropZone {
11031194
`
11041195
})
11051196
export class DraggableInDropZoneWithCustomPreview {
1197+
@ViewChild(CdkDrop) dropInstance: CdkDrop;
11061198
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
11071199
items = ['Zero', 'One', 'Two', 'Three'];
11081200
}

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,15 @@ export class CdkDrag<T = any> implements OnDestroy {
119119
/** Element that will be used as a template to create the draggable item's preview. */
120120
@ContentChild(CdkDragPreview) _previewTemplate: CdkDragPreview;
121121

122-
/**
123-
* Template for placeholder element rendered to show where a draggable would be dropped.
124-
*/
122+
/** Template for placeholder element rendered to show where a draggable would be dropped. */
125123
@ContentChild(CdkDragPlaceholder) _placeholderTemplate: CdkDragPlaceholder;
126124

127125
/** Arbitrary data to attach to this drag instance. */
128126
@Input() data: T;
129127

128+
/** Locks the position of the dragged element along the specified axis. */
129+
@Input('cdkDragLockAxis') lockAxis: 'x' | 'y';
130+
130131
/** Emits when the user starts dragging the item. */
131132
@Output('cdkDragStarted') started: EventEmitter<CdkDragStart> = new EventEmitter<CdkDragStart>();
132133

@@ -276,7 +277,7 @@ export class CdkDrag<T = any> implements OnDestroy {
276277
this._hasMoved = true;
277278
event.preventDefault();
278279

279-
const pointerPosition = this._getPointerPositionOnPage(event);
280+
const pointerPosition = this._getConstrainedPointerPosition(event);
280281

281282
if (this.dropContainer) {
282283
this._updateActiveDropContainer(pointerPosition);
@@ -361,7 +362,7 @@ export class CdkDrag<T = any> implements OnDestroy {
361362
* Updates the item's position in its drop container, or moves it
362363
* into a new one, depending on its current drag position.
363364
*/
364-
private _updateActiveDropContainer({x, y}: Point) {
365+
private _updateActiveDropContainer({x, y}) {
365366
// Drop container that draggable has been moved into.
366367
const newContainer = this.dropContainer._getSiblingContainerFromPosition(x, y);
367368

@@ -531,6 +532,20 @@ export class CdkDrag<T = any> implements OnDestroy {
531532
};
532533
}
533534

535+
/** Gets the pointer position on the page, accounting for any position constraints. */
536+
private _getConstrainedPointerPosition(event: MouseEvent | TouchEvent): Point {
537+
const point = this._getPointerPositionOnPage(event);
538+
const dropContainerLock = this.dropContainer ? this.dropContainer.lockAxis : null;
539+
540+
if (this.lockAxis === 'x' || dropContainerLock === 'x') {
541+
point.y = this._pickupPositionOnPage.y;
542+
} else if (this.lockAxis === 'y' || dropContainerLock === 'y') {
543+
point.x = this._pickupPositionOnPage.x;
544+
}
545+
546+
return point;
547+
}
548+
534549
/** Determines whether an event is a touch event. */
535550
private _isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
536551
return event.type.startsWith('touch');

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export interface CdkDropContainer<T = any> {
1919
/** Direction in which the list is oriented. */
2020
orientation: 'horizontal' | 'vertical';
2121

22+
/** Locks the position of the draggable elements inside the container along the specified axis. */
23+
lockAxis: 'x' | 'y';
24+
2225
/** Starts dragging an item. */
2326
start(): void;
2427

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
6969
*/
7070
@Input() id: string = `cdk-drop-${_uniqueIdCounter++}`;
7171

72+
/** Locks the position of the draggable elements inside the container along the specified axis. */
73+
@Input() lockAxis: 'x' | 'y';
74+
7275
/** Emits when the user drops an item inside the container. */
7376
@Output() dropped: EventEmitter<CdkDragDrop<T, any>> = new EventEmitter<CdkDragDrop<T, any>>();
7477

src/demo-app/drag-drop/drag-drop-demo.html

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ <h2>To do</h2>
44
<cdk-drop
55
#one
66
(dropped)="drop($event)"
7+
[lockAxis]="axisLock"
78
[data]="todo"
89
[connectedTo]="[two]">
910
<div *ngFor="let item of todo" cdkDrag>
@@ -18,6 +19,7 @@ <h2>Done</h2>
1819
<cdk-drop
1920
#two
2021
(dropped)="drop($event)"
22+
[lockAxis]="axisLock"
2123
[data]="done"
2224
[connectedTo]="[one]">
2325
<div *ngFor="let item of done" cdkDrag>
@@ -34,6 +36,7 @@ <h2>Horizontal list</h2>
3436
<cdk-drop
3537
orientation="horizontal"
3638
(dropped)="drop($event)"
39+
[lockAxis]="axisLock"
3740
[data]="horizontalData">
3841
<div *ngFor="let item of horizontalData" cdkDrag>
3942
{{item}}
@@ -43,14 +46,26 @@ <h2>Horizontal list</h2>
4346
</div>
4447
</div>
4548

49+
<div class="list">
50+
<h2>Free dragging</h2>
51+
<div cdkDrag class="free-draggable" [cdkDragLockAxis]="axisLock">Drag me around</div>
52+
</div>
53+
4654
<div>
4755
<h2>Data</h2>
4856
<pre>{{todo.join(', ')}}</pre>
4957
<pre>{{done.join(', ')}}</pre>
5058
<pre>{{horizontalData.join(', ')}}</pre>
5159
</div>
5260

53-
<div class="list">
54-
<h2>Free dragging</h2>
55-
<div cdkDrag class="free-draggable">Drag me around</div>
61+
<div>
62+
<h2>Axis locking</h2>
63+
<mat-form-field>
64+
<mat-label>Lock position along axis</mat-label>
65+
<mat-select [(ngModel)]="axisLock">
66+
<mat-option>None</mat-option>
67+
<mat-option value="x">X axis</mat-option>
68+
<mat-option value="y">Y axis</mat-option>
69+
</mat-select>
70+
</mat-form-field>
5671
</div>

src/demo-app/drag-drop/drag-drop-demo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk-expe
1919
encapsulation: ViewEncapsulation.None,
2020
})
2121
export class DragAndDropDemo {
22+
axisLock: 'x' | 'y';
2223
todo = [
2324
'Come up with catchy start-up name',
2425
'Add "blockchain" to name',

0 commit comments

Comments
 (0)