@mk-drag-and-drop/react 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
@@ -52,6 +52,8 @@ The React root export includes:
52
52
  - `useSortable`, `UseSortableOptions`, `UseSortableResult`
53
53
  - `useDragHandle`, `UseDragHandleResult`
54
54
  - `useRemeasureDropTargets`
55
+ - `useRecomputeActiveDrag`
56
+ - `useRemeasureOverlay`
55
57
  - `composeRefs`
56
58
  - React-friendly `restrictToContainer`, `ReactRestrictToContainerInput`
57
59
  - DOM targeting helpers, modifier helpers, lifecycle/source/result types,
@@ -222,6 +224,7 @@ type DragUpdateEvent = {
222
224
  draggableId: string;
223
225
  source: DragSource;
224
226
  pointerPosition: DragPoint;
227
+ overlayRect: DragRect | null;
225
228
  activeDropTargetId: string | null;
226
229
  previousDropTargetId: string | null;
227
230
  };
@@ -231,6 +234,7 @@ type DragEndEvent = {
231
234
  source: DragSource;
232
235
  result: DragEndResult;
233
236
  dropTargetId: string | null;
237
+ overlayRect: DragRect | null;
234
238
  };
235
239
 
236
240
  type DropEvent = {
@@ -241,6 +245,15 @@ type DropEvent = {
241
245
  };
242
246
  ```
243
247
 
248
+ `DragUpdateEvent.overlayRect` is the current viewport-space rectangle for the
249
+ drag overlay. `DragEndEvent.overlayRect` is the final viewport-space rectangle
250
+ at the moment the drag ends. React receives these DOM lifecycle event types
251
+ unchanged. The field is `null` when no drag overlay is configured. When an
252
+ overlay exists, it is based on cached overlay measurement plus modified pointer
253
+ movement, with the translated source rect as the fallback before overlay content
254
+ has been measured. React does not add overlay DOM measurement on every pointer
255
+ move.
256
+
244
257
  `onDrop` only runs for a valid successful drop. `onDragEnd.result` distinguishes
245
258
  successful drops, normal releases with no target, stale/invalid targets, and
246
259
  cancellations such as Escape or pointercancel. Plain drops are id-only.
@@ -390,6 +403,48 @@ every render. Sortable preview movement does not automatically remeasure a
390
403
  group; call this function when an app-owned layout change needs to affect
391
404
  targeting.
392
405
 
406
+ ### useRecomputeActiveDrag
407
+
408
+ `useRecomputeActiveDrag` returns a function for rerunning the active drag update
409
+ path from the last pointer position. It must be used inside `DragProvider` and
410
+ is a thin wrapper over the DOM runtime operation. It follows the same update
411
+ path as DOM pointer movement. Calling the returned function with no active drag
412
+ is safe and does nothing.
413
+
414
+ ```tsx
415
+ const recomputeActiveDrag = useRecomputeActiveDrag();
416
+
417
+ recomputeActiveDrag();
418
+ ```
419
+
420
+ Use it after external changes that require active targeting to run again without
421
+ a new pointer event. It does not remeasure drop targets. When target
422
+ measurements changed, call `useRemeasureDropTargets()` first.
423
+
424
+ ```tsx
425
+ const remeasureDropTargets = useRemeasureDropTargets();
426
+ const recomputeActiveDrag = useRecomputeActiveDrag();
427
+
428
+ remeasureDropTargets({ group: "items" });
429
+ recomputeActiveDrag();
430
+ ```
431
+
432
+ ### useRemeasureOverlay
433
+
434
+ `useRemeasureOverlay` returns a function for manually refreshing the current
435
+ overlay measurement. It must be used inside `DragProvider`. Calling the returned
436
+ function with no mounted overlay is safe and does nothing.
437
+
438
+ ```tsx
439
+ const remeasureOverlay = useRemeasureOverlay();
440
+
441
+ remeasureOverlay();
442
+ ```
443
+
444
+ Normal overlay size changes are observed automatically with `ResizeObserver`
445
+ when available. Use this hook when the app knows visual geometry changed outside
446
+ that automatic path, such as after a CSS transform or transition.
447
+
393
448
  ## Sortable Behavior
394
449
 
395
450
  Sortable preview is transient. The runtime may move DOM temporarily to show
@@ -429,8 +484,10 @@ shape.
429
484
 
430
485
  ```tsx
431
486
  <DragProvider
432
- dragOverlay={({ dragState }) => (
433
- <div className="dragOverlay">{dragState.draggableId}</div>
487
+ dragOverlay={({ dragState, remeasureOverlay }) => (
488
+ <div className="dragOverlay" onTransitionEnd={remeasureOverlay}>
489
+ {dragState.draggableId}
490
+ </div>
434
491
  )}
435
492
  >
436
493
  {children}
@@ -441,6 +498,7 @@ The overlay input includes:
441
498
 
442
499
  - `dragState`: item id, group, source rect, start pointer, and current pointer.
443
500
  - `phase`: `"dragging"` or `"released"`.
501
+ - `remeasureOverlay`: manually refreshes the cached overlay measurement.
444
502
  - `removeOverlay`: present only for `"released"` overlays in manual release
445
503
  mode; call it when app-owned release animation should remove the overlay.
446
504
 
@@ -456,10 +514,21 @@ provider, controller, or runtime teardown API.
456
514
 
457
515
  The package owns overlay hosting, movement, release, and measurement for
458
516
  targeting and modifiers. It measures overlay content on mount/replacement and
459
- uses `ResizeObserver` when available to remeasure content size changes. Dynamic
460
- overlay content should use overlay-local state, component effects, lifecycle
461
- callbacks feeding a subscription/external store, or another app-owned update
462
- path rather than relying on `dragOverlay` being called for every pointer move.
517
+ uses `ResizeObserver` when available to remeasure content size changes. The
518
+ React package mirrors DOM behavior: overlay movement remains package-managed and
519
+ imperative, so pointer movement should not require React state updates on every
520
+ move.
521
+
522
+ Use the overlay input `remeasureOverlay` helper when overlay-local UI knows its
523
+ visual bounds changed. It uses the same provider/runtime path as
524
+ `useRemeasureOverlay` and avoids reaching into internal runtime or provider
525
+ state. CSS transforms can change visual bounds without necessarily triggering
526
+ `ResizeObserver`.
527
+
528
+ Dynamic overlay content should use overlay-local state, component effects,
529
+ lifecycle callbacks feeding a subscription/external store, or another app-owned
530
+ update path rather than relying on `dragOverlay` being called for every pointer
531
+ move.
463
532
 
464
533
  ## Targeting And Modifiers
465
534
 
@@ -548,7 +617,8 @@ mount/unmount.
548
617
  `useDraggable`, `useDroppable`, and `useSortable` rely on React prop/ref updates
549
618
  for normal mount, unmount, and element replacement behavior.
550
619
 
551
- `useRemeasureDropTargets` is for layout changes, not resource release. The
620
+ `useRemeasureDropTargets`, `useRecomputeActiveDrag`, and `useRemeasureOverlay`
621
+ are for active drag geometry or targeting changes, not resource release. The
552
622
  runtime also prunes disconnected targets on drag-critical paths such as
553
623
  remeasurement.
554
624
 
@@ -584,6 +654,10 @@ React examples live in `apps/react-web/src/react`:
584
654
  - `useDragHandle()`: returns the handle attribute props for a handle element.
585
655
  - `useRemeasureDropTargets()`: returns a function for remeasuring all targets,
586
656
  one target, multiple targets, or a group.
657
+ - `useRecomputeActiveDrag()`: returns a function for rerunning active targeting
658
+ and drag update from the last pointer position without remeasuring targets.
659
+ - `useRemeasureOverlay()`: returns a function for manually refreshing the
660
+ currently mounted overlay measurement.
587
661
  - `composeRefs`: helper for combining the refs returned by hooks with app refs.
588
662
  - `lockToXAxis`, `lockToYAxis`: DOM movement modifiers re-exported by React.
589
663
  - `restrictToContainer`: React-friendly container-bound modifier.
@@ -2,5 +2,6 @@ import type { DragRuntimeScope } from "@mk-drag-and-drop/dom/integration";
2
2
  export type DragContextValue = {
3
3
  runtime: DragRuntimeScope;
4
4
  keyboardDragEnabled: boolean;
5
+ remeasureOverlay: () => void;
5
6
  };
6
7
  export declare const DragContext: import("react").Context<DragContextValue | null>;
@@ -5,13 +5,16 @@ export type DragOverlayInput = {
5
5
  dragState: DragState;
6
6
  } & ({
7
7
  phase: Extract<DragOverlayPhase, "dragging">;
8
+ remeasureOverlay: () => void;
8
9
  removeOverlay?: never;
9
10
  } | {
10
11
  phase: Extract<DragOverlayPhase, "released">;
12
+ remeasureOverlay: () => void;
11
13
  removeOverlay: () => void;
12
14
  });
13
15
  export type DragOverlayHostHandle = {
14
16
  move: (dragState: DragState) => void;
17
+ remeasure: () => void;
15
18
  };
16
19
  export declare const DragOverlayHost: import("react").NamedExoticComponent<{
17
20
  dragState: DragState;
@@ -1,7 +1,19 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { memo, useLayoutEffect, useRef, } from "react";
2
+ import { memo, useCallback, useLayoutEffect, useRef, } from "react";
3
3
  export const DragOverlayHost = memo(function DragOverlayHost({ dragState, children, contentId, onHostReady, onOverlayRectChange, }) {
4
4
  const overlayWrapperRef = useRef(null);
5
+ const measureOverlayElement = useCallback(() => {
6
+ const wrapper = overlayWrapperRef.current;
7
+ if (!wrapper) {
8
+ onOverlayRectChange?.(null);
9
+ return;
10
+ }
11
+ const measuredElement = getMeasuredOverlayElement(wrapper);
12
+ if (!measuredElement.isConnected) {
13
+ return;
14
+ }
15
+ onOverlayRectChange?.(domRectToDragRect(measuredElement.getBoundingClientRect()));
16
+ }, [onOverlayRectChange]);
5
17
  useLayoutEffect(() => {
6
18
  const wrapper = overlayWrapperRef.current;
7
19
  if (!wrapper) {
@@ -12,26 +24,21 @@ export const DragOverlayHost = memo(function DragOverlayHost({ dragState, childr
12
24
  move: (nextDragState) => {
13
25
  moveOverlayWrapper(wrapper, nextDragState);
14
26
  },
27
+ remeasure: measureOverlayElement,
15
28
  };
16
29
  onHostReady?.(host);
17
30
  host.move(dragState);
18
31
  return () => {
19
32
  onHostReady?.(null);
20
33
  };
21
- }, [dragState, onHostReady]);
34
+ }, [dragState, measureOverlayElement, onHostReady]);
22
35
  useLayoutEffect(() => {
23
36
  const wrapper = overlayWrapperRef.current;
24
37
  if (!wrapper) {
25
38
  onOverlayRectChange?.(null);
26
39
  return;
27
40
  }
28
- const measuredElement = wrapper.firstElementChild ?? wrapper;
29
- const measureOverlayElement = () => {
30
- if (!measuredElement.isConnected) {
31
- return;
32
- }
33
- onOverlayRectChange?.(domRectToDragRect(measuredElement.getBoundingClientRect()));
34
- };
41
+ const measuredElement = getMeasuredOverlayElement(wrapper);
35
42
  measureOverlayElement();
36
43
  if (typeof ResizeObserver === "undefined") {
37
44
  return () => {
@@ -46,7 +53,7 @@ export const DragOverlayHost = memo(function DragOverlayHost({ dragState, childr
46
53
  resizeObserver.disconnect();
47
54
  onOverlayRectChange?.(null);
48
55
  };
49
- }, [contentId, onOverlayRectChange]);
56
+ }, [contentId, measureOverlayElement, onOverlayRectChange]);
50
57
  return (_jsx("div", { ref: overlayWrapperRef, style: {
51
58
  position: "fixed",
52
59
  left: dragState.sourceRect.left,
@@ -58,6 +65,9 @@ export const DragOverlayHost = memo(function DragOverlayHost({ dragState, childr
58
65
  transform: getOverlayTransform(dragState),
59
66
  }, children: children }));
60
67
  });
68
+ function getMeasuredOverlayElement(wrapper) {
69
+ return wrapper.firstElementChild ?? wrapper;
70
+ }
61
71
  function moveOverlayWrapper(wrapper, dragState) {
62
72
  wrapper.style.transform = getOverlayTransform(dragState);
63
73
  }
@@ -77,10 +77,14 @@ export function DragProvider({ children, announcements, dragOverlay, keyboardCon
77
77
  });
78
78
  }
79
79
  const runtime = runtimeRef.current;
80
+ const remeasureOverlay = useCallback(() => {
81
+ overlayHostRef.current?.remeasure();
82
+ }, []);
80
83
  const contextValue = useMemo(() => ({
81
84
  runtime,
82
85
  keyboardDragEnabled,
83
- }), [keyboardDragEnabled, runtime]);
86
+ remeasureOverlay,
87
+ }), [keyboardDragEnabled, remeasureOverlay, runtime]);
84
88
  useEffect(() => {
85
89
  return () => {
86
90
  runtime.releaseActiveDragResources();
@@ -193,12 +197,14 @@ export function DragProvider({ children, announcements, dragOverlay, keyboardCon
193
197
  return {
194
198
  dragState: overlayState.dragState,
195
199
  phase: "released",
200
+ remeasureOverlay,
196
201
  removeOverlay: clearOverlayHost,
197
202
  };
198
203
  }
199
204
  return {
200
205
  dragState: overlayState.dragState,
201
206
  phase: "dragging",
207
+ remeasureOverlay,
202
208
  };
203
209
  }
204
210
  }
@@ -0,0 +1 @@
1
+ export declare function useRecomputeActiveDrag(): () => void;
@@ -0,0 +1,12 @@
1
+ import { useCallback, useContext } from "react";
2
+ import { DragContext } from "../drag-context.js";
3
+ export function useRecomputeActiveDrag() {
4
+ const context = useContext(DragContext);
5
+ if (!context) {
6
+ throw new Error("useRecomputeActiveDrag must be used inside DragProvider");
7
+ }
8
+ const { runtime } = context;
9
+ return useCallback(() => {
10
+ runtime.recomputeActiveDrag();
11
+ }, [runtime]);
12
+ }
@@ -0,0 +1 @@
1
+ export declare function useRemeasureOverlay(): () => void;
@@ -0,0 +1,12 @@
1
+ import { useCallback, useContext } from "react";
2
+ import { DragContext } from "../drag-context.js";
3
+ export function useRemeasureOverlay() {
4
+ const context = useContext(DragContext);
5
+ if (!context) {
6
+ throw new Error("useRemeasureOverlay must be used inside DragProvider");
7
+ }
8
+ const { remeasureOverlay } = context;
9
+ return useCallback(() => {
10
+ remeasureOverlay();
11
+ }, [remeasureOverlay]);
12
+ }
package/dist/index.d.ts CHANGED
@@ -8,5 +8,7 @@ export { useDraggable, type UseDraggableOptions, type UseDraggableResult, } from
8
8
  export { useDropContainer, type UseDropContainerOptions, type UseDropContainerResult, } from "./hooks/use-drop-container.js";
9
9
  export { useDroppable, type UseDroppableOptions, type UseDroppableResult, } from "./hooks/use-droppable.js";
10
10
  export { useRemeasureDropTargets } from "./hooks/use-remeasure-drop-targets.js";
11
+ export { useRecomputeActiveDrag } from "./hooks/use-recompute-active-drag.js";
12
+ export { useRemeasureOverlay } from "./hooks/use-remeasure-overlay.js";
11
13
  export { useSortable, type UseSortableOptions, type UseSortableResult, } from "./hooks/use-sortable.js";
12
14
  export { composeRefs } from "./utils/compose-refs.js";
package/dist/index.js CHANGED
@@ -6,5 +6,7 @@ export { useDraggable, } from "./hooks/use-draggable.js";
6
6
  export { useDropContainer, } from "./hooks/use-drop-container.js";
7
7
  export { useDroppable, } from "./hooks/use-droppable.js";
8
8
  export { useRemeasureDropTargets } from "./hooks/use-remeasure-drop-targets.js";
9
+ export { useRecomputeActiveDrag } from "./hooks/use-recompute-active-drag.js";
10
+ export { useRemeasureOverlay } from "./hooks/use-remeasure-overlay.js";
9
11
  export { useSortable, } from "./hooks/use-sortable.js";
10
12
  export { composeRefs } from "./utils/compose-refs.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mk-drag-and-drop/react",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "React hooks and provider for the mk drag-and-drop DOM runtime.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://mkramer.dev",
@@ -37,8 +37,9 @@
37
37
  "files": [
38
38
  "dist"
39
39
  ],
40
+ "sideEffects": false,
40
41
  "dependencies": {
41
- "@mk-drag-and-drop/dom": "0.2.0"
42
+ "@mk-drag-and-drop/dom": "^0.4.0"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "react": "^19.0.0"