ngx-virtual-dnd 3.1.2 → 3.2.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,7 @@ Inspired by [react-virtualized-dnd](https://github.com/forecast-it/react-virtual
15
15
  - **Container Constraints** - Constrain drag preview to container boundaries
16
16
  - **Axis Locking** - Lock dragging to horizontal or vertical axis
17
17
  - **Custom Previews** - Template-based drag preview and placeholder customization
18
+ - **Shift Animations** - Opt-in sliding of displaced items, plus a per-step event for haptics
18
19
  - **Keyboard Accessible** - Space to grab, arrows to move, Escape to cancel
19
20
  - **Touch Support** - Works with mouse and touch, with configurable delay/threshold
20
21
  - **Angular 21+** - Signals, standalone components, modern patterns
@@ -45,15 +46,15 @@ import {
45
46
  DragPreviewComponent,
46
47
  ],
47
48
  template: `
48
- <!-- Item template -->
49
- <ng-template #itemTpl let-item>
50
- <div class="item" [vdndDraggable]="item.id" [vdndDraggableData]="item">
51
- {{ item.name }}
52
- </div>
53
- </ng-template>
54
-
55
49
  <!-- Lists wrapped in a group -->
56
50
  <div vdndGroup="my-group">
51
+ <!-- Item template: declare it inside vdndGroup so the draggables inherit the group -->
52
+ <ng-template #itemTpl let-item>
53
+ <div class="item" [vdndDraggable]="item.id" [vdndDraggableData]="item">
54
+ {{ item.name }}
55
+ </div>
56
+ </ng-template>
57
+
57
58
  <vdnd-sortable-list
58
59
  droppableId="list-1"
59
60
  group="my-group"
@@ -98,6 +99,8 @@ export class MyComponent {
98
99
 
99
100
  **That's it!** `VirtualSortableListComponent` handles placeholder positioning, sticky items during drag, and virtual scroll integration automatically.
100
101
 
102
+ > Every draggable and droppable needs a group (from a `vdndGroup` ancestor or `vdndDraggableGroup` / `vdndDroppableGroup`), even for a single list — without one, drag is disabled. Templates resolve the group from where they are **declared**, so keep the `<ng-template>` inside the `vdndGroup` element.
103
+
101
104
  ## API Overview
102
105
 
103
106
  The library exports these main pieces (use IDE completion for full details):
@@ -241,10 +244,13 @@ The preview cannot leave the droppable area, and the placeholder snaps to the ed
241
244
  Lock dragging to a single axis:
242
245
 
243
246
  ```html
247
+ <!-- Lock the Y axis → item moves horizontally only -->
244
248
  <div [vdndDraggable]="item.id" lockAxis="y">{{ item.name }}</div>
245
249
  ```
246
250
 
247
- Values: `'x'` (horizontal only), `'y'` (vertical only), or omit for free movement.
251
+ Values: `'x'` (X axis locked → **vertical-only** movement), `'y'` (Y axis locked → **horizontal-only** movement), or omit for free movement. The value names the axis that is _frozen_.
252
+
253
+ > **Note:** This is the opposite of Angular CDK's `cdkDragLockAxis`, where `'x'` means _movement is constrained to_ the X axis (horizontal only). Here `'x'` freezes the X axis. Keep this in mind when migrating from CDK.
248
254
 
249
255
  ### Drag Threshold & Delay
250
256
 
@@ -289,14 +295,48 @@ Configure auto-scroll behavior when dragging near container edges:
289
295
  />
290
296
  ```
291
297
 
292
- | Option | Default | Description |
293
- | ------------ | ------- | ------------------------------------ |
294
- | `threshold` | `50` | Distance from edge (px) to trigger |
295
- | `maxSpeed` | `15` | Maximum scroll speed (px/frame) |
296
- | `accelerate` | `true` | Speed up based on distance from edge |
298
+ | Option | Default | Description |
299
+ | ------------ | ------- | ---------------------------------------------- |
300
+ | `threshold` | `50` | Distance from edge (px) to trigger |
301
+ | `maxSpeed` | `15` | Maximum scroll speed in pixels per 60fps frame |
302
+ | `accelerate` | `true` | Speed up based on distance from edge |
297
303
 
298
304
  Set `[autoScrollEnabled]="false"` to disable auto-scroll entirely. These options are available on `VirtualSortableListComponent`, `DroppableDirective`, and `ScrollableDirective`.
299
305
 
306
+ ### Shift Animations & Haptics
307
+
308
+ Items displaced by the placeholder jump into place by default. Provide `VDND_ANIMATION_CONFIG` to make them slide instead (a compositor-only `transform` animation, works with virtual scrolling and dynamic heights):
309
+
310
+ ```typescript
311
+ import { VDND_ANIMATION_CONFIG } from 'ngx-virtual-dnd';
312
+
313
+ // app.config.ts (app-wide) or any component's `providers` (that subtree only)
314
+ providers: [{ provide: VDND_ANIMATION_CONFIG, useValue: { shiftDuration: 200 } }];
315
+ ```
316
+
317
+ | Option | Default | Description |
318
+ | --------------- | ---------------------------- | ----------------------------------- |
319
+ | `shiftDuration` | `200` | Slide duration in ms (`0` disables) |
320
+ | `shiftEasing` | `cubic-bezier(0.2, 0, 0, 1)` | CSS easing function |
321
+
322
+ The animation is skipped when the user prefers reduced motion. If the placeholder moves again mid-slide, items continue from where they currently are. Values are read each time an animation starts, so a getter can toggle it at runtime.
323
+
324
+ For haptic feedback on every step, listen to `(placeholderMove)` on `vdndDroppable` or `vdnd-sortable-list`. It fires each time the placeholder moves within that list (every item displacement), including when it enters the list — not for the initial pick-up or when it leaves:
325
+
326
+ ```html
327
+ <vdnd-sortable-list ... (placeholderMove)="onPlaceholderMove($event)" />
328
+ ```
329
+
330
+ ```typescript
331
+ import { Haptics } from '@capacitor/haptics';
332
+
333
+ onPlaceholderMove(event: PlaceholderMoveEvent): void {
334
+ Haptics.selectionChanged(); // or navigator.vibrate?.(10) on the web
335
+ }
336
+ ```
337
+
338
+ `PlaceholderMoveEvent` carries `draggableId`, `sourceDroppableId`, `droppableId`, `previousIndex` (`null` when the placeholder just entered the list) and `currentIndex`, using the same index convention as `DropEvent.destination.index`.
339
+
300
340
  ### Disabling Drag & Drop
301
341
 
302
342
  Use the `disabled` input to conditionally disable draggables, droppables, or entire lists:
@@ -320,6 +360,8 @@ Use the `disabled` input to conditionally disable draggables, droppables, or ent
320
360
 
321
361
  Disabled draggables get the `vdnd-draggable-disabled` CSS class. Disabled droppables get `vdnd-droppable-disabled`.
322
362
 
363
+ A disabled droppable is removed from all drag-time candidate sets: pointer hit-testing skips it (the cursor falls through to whatever enabled droppable sits underneath, or none) and keyboard `ArrowLeft`/`ArrowRight` navigation steps over it. Releasing a drag over a disabled droppable fires **no `drop` event**; the `(dragEnd)` event still fires with `cancelled: false` and `destinationIndex: null`, so consumers that pair `drop`/`dragEnd` should treat a `null` `destinationIndex` as "no valid drop target".
364
+
323
365
  ### Low-Level API
324
366
 
325
367
  For maximum control, use individual components instead of `VirtualSortableListComponent`:
@@ -334,13 +376,13 @@ For maximum control, use individual components instead of `VirtualSortableListCo
334
376
  DragPreviewComponent,
335
377
  ],
336
378
  template: `
337
- <ng-template #itemTpl let-item>
338
- <div class="item" [vdndDraggable]="item.id" [vdndDraggableData]="item">
339
- {{ item.name }}
340
- </div>
341
- </ng-template>
342
-
343
379
  <div vdndGroup="demo">
380
+ <ng-template #itemTpl let-item>
381
+ <div class="item" [vdndDraggable]="item.id" [vdndDraggableData]="item">
382
+ {{ item.name }}
383
+ </div>
384
+ </ng-template>
385
+
344
386
  <div vdndDroppable="list-1" (drop)="onDrop($event)">
345
387
  <vdnd-virtual-scroll
346
388
  droppableId="list-1"
@@ -428,6 +470,7 @@ The library emits events with position data. Implement announcements in your app
428
470
  ```typescript
429
471
  @Component({
430
472
  template: `
473
+ <!-- Inside a vdndGroup, like any draggable -->
431
474
  <div
432
475
  vdndDraggable="item-1"
433
476
  (dragStart)="announce('Grabbed ' + item.name)"
@@ -446,11 +489,13 @@ export class MyComponent {
446
489
  }
447
490
 
448
491
  announceEnd(e: DragEndEvent) {
449
- this.announce(
450
- e.cancelled
451
- ? `Cancelled. Returned to position ${e.sourceIndex + 1}`
452
- : `Dropped at position ${e.destinationIndex! + 1}`,
453
- );
492
+ // destinationIndex is null when there is no valid drop target: an Escape cancel,
493
+ // a release outside every droppable, or a release over a disabled droppable.
494
+ if (e.destinationIndex === null) {
495
+ this.announce(`Returned to position ${e.sourceIndex + 1}`);
496
+ return;
497
+ }
498
+ this.announce(`Dropped at position ${e.destinationIndex + 1}`);
454
499
  }
455
500
  }
456
501
  ```
@@ -483,19 +528,29 @@ ARIA attributes (`aria-grabbed`, `aria-dropeffect`, `tabindex`) are managed auto
483
528
 
484
529
  All event types are importable from `ngx-virtual-dnd`.
485
530
 
486
- | Output | Event Type | Emitted By |
487
- | ------------- | ---------------- | ---------------------------------------------------- |
488
- | `(dragStart)` | `DragStartEvent` | `DraggableDirective` |
489
- | `(dragEnd)` | `DragEndEvent` | `DraggableDirective` |
490
- | `(drop)` | `DropEvent` | `DroppableDirective`, `VirtualSortableListComponent` |
531
+ | Output | Event Type | Emitted By |
532
+ | ------------------- | ---------------------- | ---------------------------------------------------- |
533
+ | `(dragStart)` | `DragStartEvent` | `DraggableDirective` |
534
+ | `(dragEnd)` | `DragEndEvent` | `DraggableDirective` |
535
+ | `(drop)` | `DropEvent` | `DroppableDirective`, `VirtualSortableListComponent` |
536
+ | `(placeholderMove)` | `PlaceholderMoveEvent` | `DroppableDirective`, `VirtualSortableListComponent` |
491
537
 
492
- `DragEndEvent` includes a `cancelled` boolean to distinguish drops from cancellations.
538
+ `DragEndEvent.destinationIndex` is `null` when no drop occurred — an Escape cancel, a release outside every droppable, or a release over a disabled droppable — so branch on `destinationIndex === null` to detect that. The `cancelled` boolean is `true` only for an active Escape cancel.
493
539
 
494
540
  ## How It Works
495
541
 
496
542
  Traditional drag-and-drop libraries query sibling DOM elements via `getBoundingClientRect()`. This fails with virtual scrolling because items outside the viewport aren't rendered.
497
543
 
498
- This library uses **element-under-point detection**: temporarily hide the dragged element, use `document.elementFromPoint()` to find what's at the cursor, walk up to find the droppable, and calculate placeholder position mathematically.
544
+ This library uses **geometric hit-testing against a cached rect snapshot**: at drag start it snapshots the candidate droppables of the active group and their bounding rects, then per-pointermove it hit-tests the cursor against those rects (last match in document order wins) and calculates the placeholder position mathematically. Caching the rects avoids the forced layout flush that `document.elementFromPoint()` imposes on the hot drag loop. The snapshot self-heals during the drag: rects are re-read on scroll/resize and when a candidate resizes (via `ResizeObserver`), the candidate **list** is refreshed when a droppable mounts or unmounts mid-drag, and each rect is clipped to its nearest `vdndScrollable` ancestor so a droppable scrolled out of a clipping container is not falsely hit.
545
+
546
+ ### Known Limitations
547
+
548
+ Because hit-testing is geometry against cached rects (not live `elementFromPoint`), a few browser-native behaviors are **not** reproduced:
549
+
550
+ - **Occluding overlays** — a modal, toolbar, or other element painted over a droppable does not block a geometric hit; the droppable underneath still resolves as the target.
551
+ - **Stacking contexts / explicit `z-index`** — the "last in document order" tie-break approximates paint order but is not true painter order once `z-index` and stacking contexts are involved. An earlier-in-DOM droppable raised visually on top via `z-index` loses the tie-break to a later sibling.
552
+
553
+ If your layout depends on these, keep overlapping droppables out of the same drag group, or avoid painting interactive overlays across active drop targets during a drag.
499
554
 
500
555
  ## Browser Support
501
556
 
@@ -517,7 +572,7 @@ npm run e2e # E2E tests
517
572
  Install a skill to teach AI coding assistants how to integrate this library:
518
573
 
519
574
  ```bash
520
- npx skills add gultyayev/angular-vdnd
575
+ npx skills add gultyayev/ngx-virtual-dnd
521
576
  ```
522
577
 
523
578
  Works with Claude Code, Cursor, Windsurf, GitHub Copilot, and [40+ other agents](https://skills.sh).