@mk-drag-and-drop/dom 0.2.0 → 0.4.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
@@ -36,6 +36,8 @@ The root export includes:
36
36
  - `createDropContainer`
37
37
  - `createSortable`
38
38
  - `createDragHandle`
39
+ - controller types: `DragController`, `DragControllerOptions`,
40
+ `DragControllerOverlayInput`, `DragControllerAnnouncements`
39
41
  - lifecycle types: `DragStartEvent`, `DragUpdateEvent`, `DragEndEvent`,
40
42
  `DropEvent`, `DragSource`, `DragEndResult`, `DragLifecycleCallbacks`,
41
43
  `DragLifecycleHelpers`
@@ -94,6 +96,11 @@ The returned controller has:
94
96
 
95
97
  - `remeasureDropTargets(input?)`: remeasures all targets, one target id, an
96
98
  array of target ids, or `{ group }`
99
+ - `remeasureOverlay()`: remeasures the currently mounted drag overlay element;
100
+ it is a safe no-op when no overlay is mounted or no drag is active
101
+ - `recomputeActiveDrag()`: reruns active targeting and drag update from the
102
+ last pointer position; it is a safe no-op when no drag is active and does not
103
+ remeasure drop targets
97
104
 
98
105
  The controller does not expose runtime teardown, disposal, update, or broad
99
106
  overlay removal methods. Runtime objects are ordinary JavaScript objects; DOM
@@ -108,6 +115,14 @@ controller calls `dragOverlay` once more for the `"released"` phase and passes
108
115
  `removeOverlay`; call that function when app-owned release animation is done.
109
116
  Pointer movement does not call `dragOverlay`.
110
117
 
118
+ The overlay input includes:
119
+
120
+ - `dragState`: item id, group, source rect, start pointer, and current pointer.
121
+ - `phase`: `"dragging"` or `"released"`.
122
+ - `remeasureOverlay`: manually refreshes the cached overlay measurement.
123
+ - `removeOverlay`: present only for `"released"` overlays in manual release
124
+ mode; call it when app-owned release animation should remove the overlay.
125
+
111
126
  `removeOverlay` is only present for released overlays in manual release mode.
112
127
  Calling it more than once is safe. It removes the overlay host state only; it is
113
128
  not a controller or runtime teardown API.
@@ -121,7 +136,31 @@ from the current pointer delta.
121
136
  The controller measures the returned overlay element on mount/replacement. When
122
137
  `ResizeObserver` is available, it also remeasures when that element changes
123
138
  size. The runtime derives the current overlay rect from that cached measurement
124
- plus pointer movement.
139
+ plus modified pointer movement, and it does not measure overlay DOM on every
140
+ pointer move.
141
+
142
+ Manual `remeasureOverlay()` is for cases automatic observation cannot cover or
143
+ where the app knows visual geometry changed. CSS transforms can change visual
144
+ bounds without necessarily triggering `ResizeObserver`.
145
+
146
+ ```ts
147
+ controller.remeasureOverlay();
148
+ ```
149
+
150
+ ```ts
151
+ const controller = createDragController({
152
+ dragOverlay({ dragState, remeasureOverlay }) {
153
+ const element = document.createElement("div");
154
+ element.textContent = dragState.draggableId;
155
+
156
+ element.addEventListener("transitionend", () => {
157
+ remeasureOverlay();
158
+ });
159
+
160
+ return element;
161
+ },
162
+ });
163
+ ```
125
164
 
126
165
  For dynamic overlay content, keep a reference to the element and update it from
127
166
  lifecycle callbacks or subscriptions:
@@ -164,6 +203,7 @@ type DragUpdateEvent = {
164
203
  draggableId: string;
165
204
  source: DragSource;
166
205
  pointerPosition: DragPoint;
206
+ overlayRect: DragRect | null;
167
207
  activeDropTargetId: string | null;
168
208
  previousDropTargetId: string | null;
169
209
  };
@@ -181,6 +221,7 @@ type DragEndEvent = {
181
221
  source: DragSource;
182
222
  result: DragEndResult;
183
223
  dropTargetId: string | null;
224
+ overlayRect: DragRect | null;
184
225
  };
185
226
 
186
227
  type DropEvent = {
@@ -200,6 +241,14 @@ type SortableDropPlacement = {
200
241
  };
201
242
  ```
202
243
 
244
+ `DragUpdateEvent.overlayRect` is the current viewport-space rectangle for the
245
+ drag overlay. `DragEndEvent.overlayRect` is the final viewport-space rectangle
246
+ at the moment the drag ends. Both are `null` when no drag overlay is configured.
247
+ When an overlay is configured, the runtime uses cached overlay measurement plus
248
+ modified pointer movement. Before overlay content has been measured, it uses the
249
+ same translated source-rect fallback as the visual overlay. The drag update and
250
+ drag end paths do not read overlay DOM just to produce this field.
251
+
203
252
  Helpers:
204
253
 
205
254
  - `getDropTargetRect(dropTargetId)`
@@ -339,8 +388,14 @@ modifier state.
339
388
  ## Measurement
340
389
 
341
390
  Targets are measured when registered and remeasured at drag start. The explicit
342
- remeasurement entry point is `controller.remeasureDropTargets(input?)`; call it
343
- when app-owned layout changes during a drag should affect targeting.
391
+ target remeasurement entry point is `controller.remeasureDropTargets(input?)`;
392
+ call it when app-owned layout changes during a drag should affect targeting.
393
+
394
+ Overlay measurement is separate from target measurement. Overlay content is
395
+ measured on mount/replacement and by `ResizeObserver` when available. Use
396
+ `controller.remeasureOverlay()` when the current overlay geometry should be
397
+ refreshed manually, such as after an app-owned transition or transform that
398
+ changes visual bounds without a normal size-observer notification.
344
399
 
345
400
  `RemeasureDropTargetsInput` accepts:
346
401
 
@@ -356,6 +411,36 @@ order, so app-owned layout changes such as expanding tree rows or changing
356
411
  grouped sections during a drag should call the public remeasure API when those
357
412
  changes need to affect targeting.
358
413
 
414
+ ## Active Drag Recompute
415
+
416
+ `controller.recomputeActiveDrag()` reruns the active drag update path from the
417
+ last stored raw pointer position. It reapplies modifiers, derives the overlay
418
+ rect from cached overlay measurement, recomputes the active drop target from
419
+ cached target measurements, updates the overlay host, and sends the normal drag
420
+ update notifications. This is the same update path used by pointer movement.
421
+ Calling it with no active drag is safe and does nothing.
422
+
423
+ Use it when something external changes what is under the pointer during an
424
+ active drag without producing a new pointer event.
425
+
426
+ ```ts
427
+ controller.recomputeActiveDrag();
428
+ ```
429
+
430
+ Recompute does not remeasure drop targets. If target geometry or registrations
431
+ changed and those changes should affect targeting, remeasure first and then
432
+ recompute.
433
+
434
+ ```ts
435
+ controller.remeasureDropTargets({ group: "items" });
436
+ controller.recomputeActiveDrag();
437
+ ```
438
+
439
+ ```ts
440
+ // Example: after external scrolling changes what is under the pointer
441
+ controller.recomputeActiveDrag();
442
+ ```
443
+
359
444
  ## Data Ownership
360
445
 
361
446
  The DOM package never owns application data. Item ids, target ids, group ids,
@@ -13,10 +13,12 @@ export type DragControllerAnnouncements = {
13
13
  export type DragControllerOverlayInput = {
14
14
  dragState: DragState;
15
15
  phase: "dragging";
16
+ remeasureOverlay: () => void;
16
17
  removeOverlay?: never;
17
18
  } | {
18
19
  dragState: DragState;
19
20
  phase: "released";
21
+ remeasureOverlay: () => void;
20
22
  removeOverlay: () => void;
21
23
  };
22
24
  export type DragControllerOptions = {
@@ -32,5 +34,7 @@ export type DragControllerOptions = {
32
34
  } & DragLifecycleCallbacks;
33
35
  export type DragController = {
34
36
  remeasureDropTargets: (input?: RemeasureDropTargetsInput) => void;
37
+ remeasureOverlay: () => void;
38
+ recomputeActiveDrag: () => void;
35
39
  };
36
40
  export declare function createDragController(options?: DragControllerOptions): DragController;
@@ -22,6 +22,10 @@ export function createDragController(options = {}) {
22
22
  remeasureDropTargets: (input) => {
23
23
  runtime.remeasureDropTargets(input);
24
24
  },
25
+ remeasureOverlay,
26
+ recomputeActiveDrag: () => {
27
+ runtime.recomputeActiveDrag();
28
+ },
25
29
  };
26
30
  setControllerRuntime(controller, runtime);
27
31
  return controller;
@@ -124,6 +128,9 @@ export function createDragController(options = {}) {
124
128
  }
125
129
  runtime.setOverlayRect(rectToDragRect(overlayElement.getBoundingClientRect()));
126
130
  }
131
+ function remeasureOverlay() {
132
+ measureOverlayElement();
133
+ }
127
134
  function disconnectOverlayResizeObserver() {
128
135
  overlayResizeObserver?.disconnect();
129
136
  overlayResizeObserver = null;
@@ -149,12 +156,14 @@ export function createDragController(options = {}) {
149
156
  return {
150
157
  dragState: overlayState.dragState,
151
158
  phase: "released",
159
+ remeasureOverlay,
152
160
  removeOverlay: clearOverlayHost,
153
161
  };
154
162
  }
155
163
  return {
156
164
  dragState: overlayState.dragState,
157
165
  phase: "dragging",
166
+ remeasureOverlay,
158
167
  };
159
168
  }
160
169
  function syncLiveRegion() {
@@ -11,6 +11,7 @@ export type DragRuntimeScope = DomDraggableRuntime & DomDroppableRuntime & DomDr
11
11
  configure: (input: DragRuntimeScopeConfigureInput) => void;
12
12
  cancelDrag: () => void;
13
13
  releaseActiveDragResources: () => void;
14
+ recomputeActiveDrag: () => void;
14
15
  setOverlayRect: (overlayRect: DragRect | null) => void;
15
16
  };
16
17
  export type InternalStaleDomBindingRuntime = {
@@ -5,6 +5,7 @@ export function createDragRuntimeScope(options) {
5
5
  configure: (input) => runtime.configure(input),
6
6
  cancelDrag: () => runtime.cancelDrag(),
7
7
  releaseActiveDragResources: () => runtime.releaseActiveDragResources(),
8
+ recomputeActiveDrag: () => runtime.recomputeActiveDrag(),
8
9
  setOverlayRect: (overlayRect) => runtime.setOverlayRect(overlayRect),
9
10
  requestDragStart: (input) => runtime.requestDragStart(input),
10
11
  isKeyboardDragEnabled: () => runtime.isKeyboardDragEnabled(),
@@ -43,6 +43,7 @@ export declare class DragRuntime {
43
43
  requestKeyboardDragStart(input: RequestKeyboardDragStartInput): void;
44
44
  moveKeyboardDrag(direction: KeyboardMoveDirection): void;
45
45
  updatePointer(rawPointerPosition: Point): void;
46
+ recomputeActiveDrag(): void;
46
47
  private updatePointerNow;
47
48
  private updatePointerNowOrRelease;
48
49
  private finishDragAfterQueuedPointerUpdate;
@@ -180,6 +180,13 @@ export class DragRuntime {
180
180
  }
181
181
  });
182
182
  }
183
+ recomputeActiveDrag() {
184
+ const session = this.getDraggingSession();
185
+ if (!session) {
186
+ return;
187
+ }
188
+ this.updatePointerNowOrRelease(session.rawPointerPosition);
189
+ }
183
190
  updatePointerNow(rawPointerPosition) {
184
191
  const session = this.getDraggingSession();
185
192
  if (!session) {
@@ -221,6 +228,7 @@ export class DragRuntime {
221
228
  draggableId: nextSession.draggableId,
222
229
  source: nextSession.source,
223
230
  pointerPosition,
231
+ overlayRect,
224
232
  activeDropTargetId: nextSession.activeDropTargetId,
225
233
  previousDropTargetId,
226
234
  };
@@ -499,6 +507,9 @@ export class DragRuntime {
499
507
  ? "dropped"
500
508
  : "invalid-target"
501
509
  : null;
510
+ const overlayRect = session && this.hasDragOverlay
511
+ ? this.getCurrentDragRectAt(session.pointerPosition)
512
+ : null;
502
513
  const sortablePlacement = session && dropTargetId
503
514
  ? this.getDropEventSortablePlacement(session, dropTargetId)
504
515
  : undefined;
@@ -525,6 +536,7 @@ export class DragRuntime {
525
536
  draggableId: session.draggableId,
526
537
  source: session.source,
527
538
  result,
539
+ overlayRect,
528
540
  dropTargetId,
529
541
  });
530
542
  if (result === "dropped" && dropTargetId) {
@@ -12,6 +12,7 @@ export type DragUpdateEvent = {
12
12
  draggableId: string;
13
13
  source: DragSource;
14
14
  pointerPosition: DragPoint;
15
+ overlayRect: DragRect | null;
15
16
  activeDropTargetId: string | null;
16
17
  previousDropTargetId: string | null;
17
18
  };
@@ -19,6 +20,7 @@ export type DragEndEvent = {
19
20
  draggableId: string;
20
21
  source: DragSource;
21
22
  result: DragEndResult;
23
+ overlayRect: DragRect | null;
22
24
  dropTargetId: string | null;
23
25
  };
24
26
  export type DropEvent = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mk-drag-and-drop/dom",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Headless DOM drag-and-drop primitives for draggable, droppable, sortable, and grouped interactions.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://mkramer.dev",
@@ -41,13 +41,14 @@
41
41
  "files": [
42
42
  "dist"
43
43
  ],
44
+ "sideEffects": false,
44
45
  "devDependencies": {
45
46
  "eslint": "^8.57.0",
46
47
  "jsdom": "^24.1.3",
47
48
  "typescript": "5.5.4",
48
49
  "vitest": "^2.1.9",
49
- "@repo/eslint-config": "0.0.0",
50
- "@repo/typescript-config": "0.0.0"
50
+ "@repo/typescript-config": "0.0.0",
51
+ "@repo/eslint-config": "0.0.0"
51
52
  },
52
53
  "scripts": {
53
54
  "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",