ngx-virtual-dnd 1.1.1 → 1.2.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
@@ -1,22 +1,18 @@
1
1
  # ngx-virtual-dnd
2
2
 
3
- An Angular drag-and-drop library optimized for virtual scrolling. Handles thousands of items efficiently by only rendering visible elements while maintaining smooth drag operations.
3
+ Angular drag-and-drop library optimized for virtual scrolling. Handles thousands of items efficiently by only rendering visible elements.
4
4
 
5
5
  Inspired by [react-virtualized-dnd](https://github.com/forecast-it/react-virtualized-dnd/).
6
6
 
7
7
  ## Features
8
8
 
9
- - **Virtual Scrolling** - Renders only visible items plus an overscan buffer
10
- - **Smooth Drag & Drop** - 60fps drag operations with RAF throttling
11
- - **Cross-List Support** - Drag items between multiple lists with group filtering
12
- - **Auto-Scroll** - Automatically scrolls when dragging near container edges
13
- - **Axis Locking** - Constrain dragging to horizontal or vertical axis
14
- - **Touch Support** - Works with both mouse and touch events
15
- - **Keyboard Accessible** - Full keyboard support (Space, Arrow keys, Escape)
16
- - **ARIA Support** - `aria-grabbed` and `aria-dropeffect` attributes
17
- - **Angular 21+** - Built with signals, standalone components, and modern patterns
18
- - **Simplified API** - High-level `VirtualSortableListComponent` for quick setup
19
- - **External Scroll Containers** - `vdndScrollable` directive for custom scroll hosts
9
+ - **Virtual Scrolling** - Renders only visible items plus overscan buffer
10
+ - **Smooth Drag & Drop** - 60fps with RAF throttling
11
+ - **Cross-List Support** - Drag between multiple lists with group filtering
12
+ - **Auto-Scroll** - Scrolls when dragging near container edges
13
+ - **Keyboard Accessible** - Space to grab, arrows to move, Escape to cancel
14
+ - **Touch Support** - Works with mouse and touch
15
+ - **Angular 21+** - Signals, standalone components, modern patterns
20
16
 
21
17
  ## Installation
22
18
 
@@ -24,18 +20,14 @@ Inspired by [react-virtualized-dnd](https://github.com/forecast-it/react-virtual
24
20
  npm install ngx-virtual-dnd
25
21
  ```
26
22
 
27
- **Peer Dependencies:** Angular 21+
28
-
29
- ## Quick Start (Simplified API)
30
-
31
- The easiest way to get started is with the high-level `VirtualSortableListComponent`:
23
+ ## Quick Start
32
24
 
33
25
  ```typescript
34
26
  import {
35
- DragPreviewComponent,
36
- DraggableDirective,
37
27
  VirtualSortableListComponent,
38
28
  DroppableGroupDirective,
29
+ DraggableDirective,
30
+ DragPreviewComponent,
39
31
  PlaceholderComponent,
40
32
  DropEvent,
41
33
  moveItem,
@@ -43,17 +35,17 @@ import {
43
35
 
44
36
  @Component({
45
37
  imports: [
46
- DragPreviewComponent,
47
- DraggableDirective,
48
38
  VirtualSortableListComponent,
49
39
  DroppableGroupDirective,
40
+ DraggableDirective,
41
+ DragPreviewComponent,
50
42
  PlaceholderComponent,
51
43
  ],
52
44
  template: `
53
45
  <!-- Item template -->
54
46
  <ng-template #itemTpl let-item let-isPlaceholder="isPlaceholder">
55
47
  @if (isPlaceholder) {
56
- <vdnd-placeholder [height]="50"></vdnd-placeholder>
48
+ <vdnd-placeholder [height]="50" />
57
49
  } @else {
58
50
  <div class="item" vdndDraggable="{{ item.id }}" [vdndDraggableData]="item">
59
51
  {{ item.name }}
@@ -61,7 +53,7 @@ import {
61
53
  }
62
54
  </ng-template>
63
55
 
64
- <!-- Use vdndGroup to set group for all children -->
56
+ <!-- Lists wrapped in a group -->
65
57
  <div vdndGroup="my-group">
66
58
  <vdnd-sortable-list
67
59
  droppableId="list-1"
@@ -86,16 +78,16 @@ import {
86
78
  />
87
79
  </div>
88
80
 
81
+ <!-- Required: renders the dragged item preview -->
89
82
  <vdnd-drag-preview />
90
83
  `,
91
84
  })
92
85
  export class MyComponent {
93
- list1 = signal<Item[]>([]);
94
- list2 = signal<Item[]>([]);
86
+ list1 = signal<Item[]>([...]);
87
+ list2 = signal<Item[]>([...]);
95
88
 
96
89
  getItemId = (item: Item) => item.id;
97
90
 
98
- // One-liner drop handler using moveItem utility!
99
91
  onDrop(event: DropEvent): void {
100
92
  moveItem(event, {
101
93
  'list-1': this.list1,
@@ -105,66 +97,75 @@ export class MyComponent {
105
97
  }
106
98
  ```
107
99
 
108
- ## Quick Start (Low-Level API)
100
+ **That's it!** `VirtualSortableListComponent` handles placeholder positioning, sticky items during drag, and virtual scroll integration automatically.
109
101
 
110
- For more control, use the individual directives and components:
102
+ ## API Overview
111
103
 
112
- ```typescript
113
- import {
114
- DragPreviewComponent,
115
- DraggableDirective,
116
- DroppableDirective,
117
- VirtualScrollContainerComponent,
118
- PlaceholderComponent,
119
- DropEvent,
120
- DragStateService,
121
- END_OF_LIST,
122
- } from 'ngx-virtual-dnd';
104
+ The library exports these main pieces (use IDE completion for full details):
105
+
106
+ **Components:**
123
107
 
108
+ - `VirtualSortableListComponent` - High-level component combining droppable, virtual scroll, and placeholder
109
+ - `VirtualScrollContainerComponent` - Low-level virtual scroll container
110
+ - `VirtualViewportComponent` - Self-contained virtual scroll viewport
111
+ - `VirtualContentComponent` - Virtual content for external scroll containers (page-level scroll)
112
+ - `DragPreviewComponent` - Renders the dragged item preview (required)
113
+ - `PlaceholderComponent` - Drop position indicator
114
+
115
+ **Directives:**
116
+
117
+ - `DraggableDirective` (`vdndDraggable`) - Makes an element draggable
118
+ - `DroppableDirective` (`vdndDroppable`) - Marks a drop target
119
+ - `DroppableGroupDirective` (`vdndGroup`) - Provides group context to children
120
+ - `ScrollableDirective` (`vdndScrollable`) - Marks external scroll container
121
+ - `VirtualForDirective` (`*vdndVirtualFor`) - Structural directive for virtual lists
122
+
123
+ **Services:**
124
+
125
+ - `DragStateService` - Access drag state (isDragging, draggedItem, placeholderIndex, etc.)
126
+ - `AutoScrollService` - Controls edge auto-scrolling
127
+ - `PositionCalculatorService` - Calculates placeholder positions
128
+
129
+ **Utilities:**
130
+
131
+ - `moveItem()` - Move between signal-based lists
132
+ - `reorderItems()` - Reorder within a single list
133
+ - `applyMove()` - Immutable version (returns new arrays)
134
+ - `isNoOpDrop()` - Check if drop would be a no-op
135
+ - `insertAt()` / `removeAt()` - Low-level array helpers
136
+
137
+ ## Advanced Usage
138
+
139
+ ### Low-Level API
140
+
141
+ For maximum control, use individual components instead of `VirtualSortableListComponent`:
142
+
143
+ ```typescript
124
144
  @Component({
125
145
  imports: [
126
- DragPreviewComponent,
127
- DraggableDirective,
128
- DroppableDirective,
129
146
  VirtualScrollContainerComponent,
147
+ DroppableDirective,
148
+ DraggableDirective,
149
+ DragPreviewComponent,
130
150
  PlaceholderComponent,
131
151
  ],
132
152
  template: `
133
- <div class="list" vdndDroppable="my-list" vdndDroppableGroup="demo" (drop)="onDrop($event)">
153
+ <div vdndDroppable="list-1" vdndDroppableGroup="demo" (drop)="onDrop($event)">
134
154
  <vdnd-virtual-scroll
135
155
  [items]="itemsWithPlaceholder()"
136
156
  [itemHeight]="50"
137
157
  [stickyItemIds]="stickyIds()"
138
158
  [itemIdFn]="getItemId"
139
159
  [trackByFn]="trackById"
140
- >
141
- <ng-template let-item let-index="index">
142
- @if (item.isPlaceholder) {
143
- <vdnd-placeholder [height]="50" />
144
- } @else {
145
- <div
146
- class="item"
147
- vdndDraggable="{{ item.id }}"
148
- vdndDraggableGroup="demo"
149
- [vdndDraggableData]="item"
150
- >
151
- {{ item.name }}
152
- </div>
153
- }
154
- </ng-template>
155
- </vdnd-virtual-scroll>
160
+ [itemTemplate]="itemTpl"
161
+ />
156
162
  </div>
157
-
158
163
  <vdnd-drag-preview />
159
164
  `,
160
165
  })
161
166
  export class ListComponent {
162
167
  readonly #dragState = inject(DragStateService);
163
-
164
- items = signal<Item[]>([
165
- { id: '1', name: 'Item 1' },
166
- { id: '2', name: 'Item 2' },
167
- ]);
168
+ items = signal<Item[]>([]);
168
169
 
169
170
  // Keep dragged item rendered during scroll
170
171
  stickyIds = computed(() => {
@@ -178,7 +179,7 @@ export class ListComponent {
178
179
  const activeDroppable = this.#dragState.activeDroppableId();
179
180
  const placeholderIndex = this.#dragState.placeholderIndex();
180
181
 
181
- if (activeDroppable !== 'my-list' || placeholderIndex === null) {
182
+ if (activeDroppable !== 'list-1' || placeholderIndex === null) {
182
183
  return items;
183
184
  }
184
185
 
@@ -186,314 +187,85 @@ export class ListComponent {
186
187
  result.splice(placeholderIndex, 0, { id: 'placeholder', isPlaceholder: true } as any);
187
188
  return result;
188
189
  });
189
-
190
- onDrop(event: DropEvent): void {
191
- const sourceIndex = event.source.index;
192
- const destIndex = event.destination.index;
193
-
194
- this.items.update((items) => {
195
- const newItems = [...items];
196
- const [removed] = newItems.splice(sourceIndex, 1);
197
- newItems.splice(destIndex, 0, removed);
198
- return newItems;
199
- });
200
- }
201
-
202
- readonly getItemId = (item: Item): string => item.id;
203
- readonly trackById = (_: number, item: Item): string => item.id;
204
- }
205
- ```
206
-
207
- ## API Reference
208
-
209
- ### DraggableDirective
210
-
211
- Makes an element draggable.
212
-
213
- ```html
214
- <div
215
- vdndDraggable="unique-id"
216
- vdndDraggableGroup="group-name"
217
- [vdndDraggableData]="data"
218
- [disabled]="false"
219
- [dragHandle]=".handle"
220
- [dragThreshold]="5"
221
- (dragStart)="onDragStart($event)"
222
- (dragMove)="onDragMove($event)"
223
- (dragEnd)="onDragEnd($event)"
224
- ></div>
225
- ```
226
-
227
- | Input | Type | Description |
228
- | -------------------- | --------- | ------------------------------------------------ |
229
- | `vdndDraggable` | `string` | Unique identifier for the draggable |
230
- | `vdndDraggableGroup` | `string` | Group name for restricting drop targets |
231
- | `vdndDraggableData` | `unknown` | Optional data attached to the draggable |
232
- | `disabled` | `boolean` | Whether dragging is disabled |
233
- | `dragHandle` | `string` | CSS selector for drag handle element |
234
- | `dragThreshold` | `number` | Minimum distance before drag starts (default: 5) |
235
-
236
- | Output | Type | Description |
237
- | ----------- | ---------------- | ---------------------------- |
238
- | `dragStart` | `DragStartEvent` | Emitted when drag starts |
239
- | `dragMove` | `DragMoveEvent` | Emitted during drag movement |
240
- | `dragEnd` | `DragEndEvent` | Emitted when drag ends |
241
-
242
- ### DroppableDirective
243
-
244
- Marks an element as a valid drop target.
245
-
246
- ```html
247
- <div
248
- vdndDroppable="list-id"
249
- vdndDroppableGroup="group-name"
250
- [vdndDroppableData]="data"
251
- [disabled]="false"
252
- [autoScrollEnabled]="true"
253
- [autoScrollConfig]="{ threshold: 50, maxSpeed: 15 }"
254
- (dragEnter)="onDragEnter($event)"
255
- (dragLeave)="onDragLeave($event)"
256
- (dragOver)="onDragOver($event)"
257
- (drop)="onDrop($event)"
258
- ></div>
259
- ```
260
-
261
- | Input | Type | Description |
262
- | -------------------- | ------------------ | --------------------------------------------- |
263
- | `vdndDroppable` | `string` | Unique identifier for the droppable |
264
- | `vdndDroppableGroup` | `string` | Group name (must match draggable group) |
265
- | `vdndDroppableData` | `unknown` | Optional data attached to the droppable |
266
- | `disabled` | `boolean` | Whether dropping is disabled |
267
- | `autoScrollEnabled` | `boolean` | Enable auto-scroll near edges (default: true) |
268
- | `autoScrollConfig` | `AutoScrollConfig` | Auto-scroll configuration |
269
-
270
- | Output | Type | Description |
271
- | ----------- | ---------------- | ----------------------------------------------- |
272
- | `dragEnter` | `DragEnterEvent` | Emitted when a draggable enters |
273
- | `dragLeave` | `DragLeaveEvent` | Emitted when a draggable leaves |
274
- | `dragOver` | `DragOverEvent` | Emitted while hovering with placeholder updates |
275
- | `drop` | `DropEvent` | Emitted when an item is dropped |
276
-
277
- ### VirtualScrollContainerComponent
278
-
279
- A virtual scroll container that only renders visible items.
280
-
281
- ```html
282
- <vdnd-virtual-scroll
283
- [items]="items()"
284
- [itemHeight]="50"
285
- [containerHeight]="400"
286
- [overscan]="3"
287
- [stickyItemIds]="stickyIds()"
288
- [itemIdFn]="getItemId"
289
- [trackByFn]="trackById"
290
- [itemTemplate]="itemTpl"
291
- (visibleRangeChange)="onRangeChange($event)"
292
- (scrollPositionChange)="onScroll($event)"
293
- >
294
- </vdnd-virtual-scroll>
295
- ```
296
-
297
- | Input | Type | Description |
298
- | ----------------- | ------------------------------------ | ------------------------------------------------- |
299
- | `items` | `T[]` | Array of items to render |
300
- | `itemHeight` | `number` | Height of each item in pixels |
301
- | `containerHeight` | `number` | Height of the container in pixels |
302
- | `overscan` | `number` | Items to render above/below viewport (default: 3) |
303
- | `stickyItemIds` | `string[]` | IDs of items that should always be rendered |
304
- | `itemIdFn` | `(item: T) => string` | Function to get unique ID from item |
305
- | `trackByFn` | `(index: number, item: T) => string` | Track-by function for the loop |
306
- | `itemTemplate` | `TemplateRef` | Template for rendering each item |
307
-
308
- ### VirtualSortableListComponent
309
-
310
- A high-level component that combines droppable, virtual scroll, and placeholder functionality. **Recommended for most use cases.**
311
-
312
- ```html
313
- <vdnd-sortable-list
314
- droppableId="list-1"
315
- group="my-group"
316
- [items]="items()"
317
- [itemHeight]="50"
318
- [itemIdFn]="getItemId"
319
- [itemTemplate]="itemTpl"
320
- [placeholderTemplate]="placeholderTpl"
321
- (drop)="onDrop($event)"
322
- >
323
- </vdnd-sortable-list>
324
- ```
325
-
326
- | Input | Type | Description |
327
- | --------------------- | --------------------- | ---------------------------------------- |
328
- | `droppableId` | `string` | Unique identifier for this list |
329
- | `group` | `string` | Drag-and-drop group name |
330
- | `items` | `T[]` | Array of items to render |
331
- | `itemHeight` | `number` | Height of each item in pixels |
332
- | `itemIdFn` | `(item: T) => string` | Function to get unique ID from item |
333
- | `itemTemplate` | `TemplateRef` | Template for rendering each item |
334
- | `trackByFn` | `Function` | Optional track-by (defaults to itemIdFn) |
335
- | `placeholderTemplate` | `TemplateRef` | Optional custom placeholder template |
336
- | `containerHeight` | `number` | Optional explicit container height |
337
- | `disabled` | `boolean` | Whether this list is disabled |
338
-
339
- ### DroppableGroupDirective
340
-
341
- Provides group context to child draggables and droppables.
342
-
343
- ```html
344
- <!-- Without group directive (verbose) -->
345
- <div vdndDroppable="list-1" vdndDroppableGroup="my-group">
346
- <div vdndDraggable="item-1" vdndDraggableGroup="my-group">Item 1</div>
347
- </div>
348
-
349
- <!-- With group directive (concise) -->
350
- <div vdndGroup="my-group">
351
- <div vdndDroppable="list-1">
352
- <div vdndDraggable="item-1">Item 1</div>
353
- </div>
354
- </div>
355
- ```
356
-
357
- ### Drop Utilities
358
-
359
- ```typescript
360
- import { moveItem, reorderItems, applyMove, isNoOpDrop } from 'ngx-virtual-dnd';
361
-
362
- // Move items between signal-based lists
363
- moveItem(event, {
364
- 'list-1': this.list1,
365
- 'list-2': this.list2,
366
- });
367
-
368
- // Reorder within a single list
369
- reorderItems(event, this.items);
370
-
371
- // Immutable version (returns new arrays)
372
- const updated = applyMove(event, {
373
- 'list-1': this.list1(),
374
- 'list-2': this.list2(),
375
- });
376
-
377
- // Check if drop is a no-op (same position)
378
- if (isNoOpDrop(event)) return;
379
- ```
380
-
381
- ### DragStateService
382
-
383
- Central service for accessing drag state. Inject to build custom integrations.
384
-
385
- ```typescript
386
- @Injectable({ providedIn: 'root' })
387
- export class DragStateService {
388
- readonly isDragging: Signal<boolean>;
389
- readonly draggedItem: Signal<DraggedItem | null>;
390
- readonly sourceDroppableId: Signal<string | null>;
391
- readonly activeDroppableId: Signal<string | null>;
392
- readonly placeholderId: Signal<string | null>;
393
- readonly placeholderIndex: Signal<number | null>;
394
- readonly cursorPosition: Signal<CursorPosition | null>;
395
190
  }
396
191
  ```
397
192
 
398
- ## Event Types
193
+ ### Page-Level Scroll
399
194
 
400
- ### DragStartEvent
195
+ Use `VirtualContentComponent` with `vdndScrollable` for page-level scrolling with headers/footers:
401
196
 
402
197
  ```typescript
403
- interface DragStartEvent {
404
- draggableId: string;
405
- droppableId: string;
406
- data?: unknown;
407
- position: { x: number; y: number };
408
- sourceIndex: number; // 0-indexed position in source list
409
- }
410
- ```
411
-
412
- ### DragMoveEvent
413
-
414
- ```typescript
415
- interface DragMoveEvent {
416
- draggableId: string;
417
- position: { x: number; y: number };
418
- targetIndex: number | null; // Current placeholder index (0-indexed)
419
- }
420
- ```
421
-
422
- ### DragEndEvent
423
-
424
- ```typescript
425
- interface DragEndEvent {
426
- draggableId: string;
427
- position: { x: number; y: number };
428
- cancelled: boolean;
429
- sourceIndex: number; // Original position (for announcements)
430
- destinationIndex: number | null; // Final position (null if cancelled)
431
- }
432
- ```
433
-
434
- ### DropEvent
435
-
436
- ```typescript
437
- interface DropEvent {
438
- source: {
439
- draggableId: string;
440
- droppableId: string;
441
- index: number;
442
- data?: unknown;
443
- };
444
- destination: {
445
- droppableId: string;
446
- placeholderId: string;
447
- index: number;
448
- data?: unknown;
449
- };
450
- }
451
- ```
198
+ @Component({
199
+ imports: [
200
+ ScrollableDirective,
201
+ VirtualContentComponent,
202
+ VirtualForDirective,
203
+ DraggableDirective,
204
+ DroppableDirective,
205
+ DroppableGroupDirective,
206
+ DragPreviewComponent,
207
+ ],
208
+ template: `
209
+ <ion-content [scrollY]="false">
210
+ <div class="scroll-container ion-content-scroll-host" vdndScrollable>
211
+ <!-- Header that scrolls away -->
212
+ <div class="header" #header>Welcome!</div>
213
+
214
+ <!-- Virtual list -->
215
+ <div vdndGroup="tasks">
216
+ <vdnd-virtual-content
217
+ [itemHeight]="72"
218
+ [totalItems]="items().length"
219
+ [contentOffset]="headerHeight()"
220
+ [style.height.px]="items().length * 72"
221
+ vdndDroppable="list-1"
222
+ (drop)="onDrop($event)"
223
+ >
224
+ <ng-container
225
+ *vdndVirtualFor="
226
+ let item of items();
227
+ itemHeight: 72;
228
+ trackBy: trackById;
229
+ droppableId: 'list-1'
230
+ "
231
+ >
232
+ <div class="item" [vdndDraggable]="item.id">{{ item.name }}</div>
233
+ </ng-container>
234
+ </vdnd-virtual-content>
235
+ </div>
452
236
 
453
- ### AutoScrollConfig
237
+ <!-- Footer -->
238
+ <div class="footer">Load more</div>
239
+ </div>
240
+ </ion-content>
454
241
 
455
- ```typescript
456
- interface AutoScrollConfig {
457
- threshold: number; // Distance from edge to start scrolling (default: 50)
458
- maxSpeed: number; // Maximum scroll speed in px/frame (default: 15)
459
- accelerate: boolean; // Accelerate based on distance from edge (default: true)
242
+ <vdnd-drag-preview />
243
+ `,
244
+ })
245
+ export class PageComponent {
246
+ items = signal<Item[]>([...]);
247
+ headerHeight = signal(0);
248
+
249
+ // Track header height with ResizeObserver
250
+ constructor() {
251
+ afterNextRender(() => {
252
+ const header = this.header().nativeElement;
253
+ this.headerHeight.set(header.offsetHeight);
254
+ });
255
+ }
460
256
  }
461
257
  ```
462
258
 
463
- ## Keyboard Accessibility
464
-
465
- The library provides full keyboard support for drag-and-drop operations, compliant with WCAG 2.5.7 (Dragging Movements) and WCAG 2.1.1 (Keyboard).
466
-
467
- ### Keyboard Interaction Model
468
-
469
- | Key | Action |
470
- | ----------------- | ------------------------------------------------ |
471
- | `Tab` | Navigate to draggable items |
472
- | `Space` | Start drag (while focused on draggable item) |
473
- | `Arrow ↑` / `↓` | Move item up/down in the list |
474
- | `Arrow ←` / `→` | Move item to adjacent list (cross-list movement) |
475
- | `Space` / `Enter` | Drop item at current position |
476
- | `Escape` | Cancel drag and return item to original position |
259
+ Key points:
477
260
 
478
- ### ARIA Attributes
479
-
480
- The library automatically manages these ARIA attributes:
481
-
482
- | Attribute | Element | Description |
483
- | ----------------- | --------- | -------------------------------------------- |
484
- | `aria-grabbed` | Draggable | `true` when being dragged, `false` otherwise |
485
- | `aria-dropeffect` | Droppable | Set to `move` on drop target containers |
486
- | `tabindex` | Draggable | Set to `0` for keyboard focusability |
261
+ - `vdndScrollable` marks the scroll container
262
+ - `VirtualContentComponent` provides wrapper-based positioning
263
+ - `contentOffset` accounts for content above the list (headers)
264
+ - Set explicit height on `vdnd-virtual-content` matching total item height
487
265
 
488
266
  ### Screen Reader Announcements
489
267
 
490
- The library emits events with position data (`sourceIndex`, `targetIndex`, `destinationIndex`) that consumers can use to build screen reader announcements. This approach:
491
-
492
- - Avoids i18n complexity (consumers control announcement text)
493
- - Allows customization of announcement timing and content
494
- - Works with any screen reader announcement mechanism
495
-
496
- **Example: Consumer-Side Announcer**
268
+ The library emits events with position data. Implement announcements in your app:
497
269
 
498
270
  ```typescript
499
271
  @Component({
@@ -505,125 +277,71 @@ The library emits events with position data (`sourceIndex`, `targetIndex`, `dest
505
277
  >
506
278
  {{ item.name }}
507
279
  </div>
508
- <div aria-live="assertive" class="visually-hidden">{{ announcement() }}</div>
280
+ <div aria-live="assertive" class="sr-only">{{ announcement() }}</div>
509
281
  `,
510
282
  })
511
283
  export class MyComponent {
512
284
  announcement = signal('');
513
285
 
514
- announce(message: string): void {
515
- this.announcement.set(message);
286
+ announce(msg: string) {
287
+ this.announcement.set(msg);
516
288
  }
517
289
 
518
- announceEnd(event: DragEndEvent): void {
519
- if (event.cancelled) {
520
- this.announce(`Cancelled. Returned to position ${event.sourceIndex + 1}`);
521
- } else {
522
- this.announce(`Dropped at position ${event.destinationIndex! + 1}`);
523
- }
290
+ announceEnd(e: DragEndEvent) {
291
+ this.announce(
292
+ e.cancelled
293
+ ? `Cancelled. Returned to position ${e.sourceIndex + 1}`
294
+ : `Dropped at position ${e.destinationIndex! + 1}`,
295
+ );
524
296
  }
525
297
  }
526
298
  ```
527
299
 
528
- ### Virtual Scroll Integration
300
+ ## Keyboard Navigation
529
301
 
530
- Keyboard navigation works seamlessly with virtual scrolling:
302
+ | Key | Action |
303
+ | ----------- | --------------------------- |
304
+ | `Tab` | Navigate to draggable items |
305
+ | `Space` | Start/end drag |
306
+ | `Arrow ↑/↓` | Move item up/down |
307
+ | `Arrow ←/→` | Move to adjacent list |
308
+ | `Escape` | Cancel drag |
531
309
 
532
- - **Auto-scroll**: When navigating beyond the visible range, the container automatically scrolls
533
- - **Large lists**: Tested with 10,000+ items without performance degradation
534
- - **Cross-list moves**: Works across multiple virtualized lists
310
+ ARIA attributes (`aria-grabbed`, `aria-dropeffect`, `tabindex`) are managed automatically.
535
311
 
536
312
  ## CSS Classes
537
313
 
538
- The library adds CSS classes for styling drag states:
539
-
540
- | Class | Element | Applied When |
541
- | ------------------------- | --------- | --------------------------- |
542
- | `vdnd-draggable` | Draggable | Always |
543
- | `vdnd-draggable-dragging` | Draggable | While being dragged |
544
- | `vdnd-draggable-disabled` | Draggable | When disabled |
545
- | `vdnd-droppable` | Droppable | Always |
546
- | `vdnd-droppable-active` | Droppable | When a draggable is over it |
547
- | `vdnd-droppable-disabled` | Droppable | When disabled |
314
+ | Class | Applied When |
315
+ | ------------------------- | --------------------------- |
316
+ | `vdnd-draggable` | Always on draggable |
317
+ | `vdnd-draggable-dragging` | While being dragged |
318
+ | `vdnd-draggable-disabled` | When disabled |
319
+ | `vdnd-drag-pending` | After delay, ready to drag |
320
+ | `vdnd-droppable` | Always on droppable |
321
+ | `vdnd-droppable-active` | When a draggable is over it |
322
+ | `vdnd-droppable-disabled` | When disabled |
548
323
 
549
324
  ## How It Works
550
325
 
551
- ### Why Virtual Scroll + DnD is Hard
552
-
553
- Traditional drag-and-drop libraries (like Angular CDK) query sibling DOM elements via `getBoundingClientRect()`. This fails with virtual scrolling because items outside the viewport aren't rendered.
554
-
555
- ### The Solution
556
-
557
- This library uses **element-under-point detection** instead of DOM sibling queries:
326
+ Traditional drag-and-drop libraries query sibling DOM elements via `getBoundingClientRect()`. This fails with virtual scrolling because items outside the viewport aren't rendered.
558
327
 
559
- 1. Temporarily hide the dragged element
560
- 2. Use `document.elementFromPoint()` to find what's at the cursor
561
- 3. Walk up the DOM to find the droppable/draggable parent
562
- 4. Calculate placeholder position mathematically
328
+ 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.
563
329
 
564
- This works because only the visible item at the cursor position needs to exist in the DOM.
565
-
566
- ### Placeholder Index
567
-
568
- The placeholder index is calculated using the **preview center position**, providing intuitive UX where the placeholder appears where the preview visually is.
569
-
570
- ## Project Structure
330
+ ## Browser Support
571
331
 
572
- ```
573
- /projects/ngx-virtual-dnd/ # Reusable library (npm: ngx-virtual-dnd)
574
- /src/ # Demo application
575
- /e2e/ # Playwright E2E tests
576
- /docs/ # Documentation
577
- ```
332
+ - Chrome/Edge (latest)
333
+ - Firefox (latest)
334
+ - Safari (latest)
578
335
 
579
336
  ## Development
580
337
 
581
338
  ```bash
582
- # Install dependencies
583
- npm install
584
-
585
- # Start demo app (http://localhost:4200)
586
- npm start
587
-
588
- # Build library (required after editing library files)
589
- ng build ngx-virtual-dnd
590
-
591
- # Run unit tests
592
- npm test
593
-
594
- # Run E2E tests
595
- npm run e2e
596
-
597
- # Run E2E tests with UI
598
- npm run e2e:ui
599
-
600
- # Lint
601
- npm run lint
602
-
603
- # Storybook
604
- npm run storybook
339
+ npm start # Dev server (localhost:4200)
340
+ ng build ngx-virtual-dnd # Build library (required after lib edits)
341
+ npm test # Unit tests
342
+ npm run e2e # E2E tests
605
343
  ```
606
344
 
607
- ### Library Development
608
-
609
- The demo app imports from `dist/ngx-virtual-dnd`. After editing any file in `/projects/ngx-virtual-dnd/`:
610
-
611
- 1. Rebuild the library: `ng build ngx-virtual-dnd`
612
- 2. Restart the dev server if running
613
-
614
- ## Documentation
615
-
616
- - [Architecture Overview](./docs/ARCHITECTURE.md) - How the library works
617
- - [API Reference](./docs/API.md) - Components, directives, and services
618
- - [Usage Guide](./docs/USAGE.md) - Detailed examples
619
- - [Algorithm](./docs/ALGORITHM.md) - Core positioning algorithm
620
-
621
- ## Browser Support
622
-
623
- - Chrome/Edge (latest)
624
- - Firefox (latest)
625
- - Safari (latest)
626
-
627
345
  ## License
628
346
 
629
347
  MIT