@mk-drag-and-drop/react 0.2.0 → 0.3.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,7 @@ The React root export includes:
52
52
  - `useSortable`, `UseSortableOptions`, `UseSortableResult`
53
53
  - `useDragHandle`, `UseDragHandleResult`
54
54
  - `useRemeasureDropTargets`
55
+ - `useRemeasureOverlay`
55
56
  - `composeRefs`
56
57
  - React-friendly `restrictToContainer`, `ReactRestrictToContainerInput`
57
58
  - DOM targeting helpers, modifier helpers, lifecycle/source/result types,
@@ -222,6 +223,7 @@ type DragUpdateEvent = {
222
223
  draggableId: string;
223
224
  source: DragSource;
224
225
  pointerPosition: DragPoint;
226
+ overlayRect: DragRect | null;
225
227
  activeDropTargetId: string | null;
226
228
  previousDropTargetId: string | null;
227
229
  };
@@ -231,6 +233,7 @@ type DragEndEvent = {
231
233
  source: DragSource;
232
234
  result: DragEndResult;
233
235
  dropTargetId: string | null;
236
+ overlayRect: DragRect | null;
234
237
  };
235
238
 
236
239
  type DropEvent = {
@@ -241,6 +244,15 @@ type DropEvent = {
241
244
  };
242
245
  ```
243
246
 
247
+ `DragUpdateEvent.overlayRect` is the current viewport-space rectangle for the
248
+ drag overlay. `DragEndEvent.overlayRect` is the final viewport-space rectangle
249
+ at the moment the drag ends. React receives these DOM lifecycle event types
250
+ unchanged. The field is `null` when no drag overlay is configured. When an
251
+ overlay exists, it is based on cached overlay measurement plus modified pointer
252
+ movement, with the translated source rect as the fallback before overlay content
253
+ has been measured. React does not add overlay DOM measurement on every pointer
254
+ move.
255
+
244
256
  `onDrop` only runs for a valid successful drop. `onDragEnd.result` distinguishes
245
257
  successful drops, normal releases with no target, stale/invalid targets, and
246
258
  cancellations such as Escape or pointercancel. Plain drops are id-only.
@@ -390,6 +402,22 @@ every render. Sortable preview movement does not automatically remeasure a
390
402
  group; call this function when an app-owned layout change needs to affect
391
403
  targeting.
392
404
 
405
+ ### useRemeasureOverlay
406
+
407
+ `useRemeasureOverlay` returns a function for manually refreshing the current
408
+ overlay measurement. It must be used inside `DragProvider`. Calling the returned
409
+ function with no mounted overlay is safe and does nothing.
410
+
411
+ ```tsx
412
+ const remeasureOverlay = useRemeasureOverlay();
413
+
414
+ remeasureOverlay();
415
+ ```
416
+
417
+ Normal overlay size changes are observed automatically with `ResizeObserver`
418
+ when available. Use this hook when the app knows visual geometry changed outside
419
+ that automatic path, such as after a CSS transform or transition.
420
+
393
421
  ## Sortable Behavior
394
422
 
395
423
  Sortable preview is transient. The runtime may move DOM temporarily to show
@@ -429,8 +457,10 @@ shape.
429
457
 
430
458
  ```tsx
431
459
  <DragProvider
432
- dragOverlay={({ dragState }) => (
433
- <div className="dragOverlay">{dragState.draggableId}</div>
460
+ dragOverlay={({ dragState, remeasureOverlay }) => (
461
+ <div className="dragOverlay" onTransitionEnd={remeasureOverlay}>
462
+ {dragState.draggableId}
463
+ </div>
434
464
  )}
435
465
  >
436
466
  {children}
@@ -441,6 +471,7 @@ The overlay input includes:
441
471
 
442
472
  - `dragState`: item id, group, source rect, start pointer, and current pointer.
443
473
  - `phase`: `"dragging"` or `"released"`.
474
+ - `remeasureOverlay`: manually refreshes the cached overlay measurement.
444
475
  - `removeOverlay`: present only for `"released"` overlays in manual release
445
476
  mode; call it when app-owned release animation should remove the overlay.
446
477
 
@@ -456,10 +487,21 @@ provider, controller, or runtime teardown API.
456
487
 
457
488
  The package owns overlay hosting, movement, release, and measurement for
458
489
  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.
490
+ uses `ResizeObserver` when available to remeasure content size changes. The
491
+ React package mirrors DOM behavior: overlay movement remains package-managed and
492
+ imperative, so pointer movement should not require React state updates on every
493
+ move.
494
+
495
+ Use the overlay input `remeasureOverlay` helper when overlay-local UI knows its
496
+ visual bounds changed. It uses the same provider/runtime path as
497
+ `useRemeasureOverlay` and avoids reaching into internal runtime or provider
498
+ state. CSS transforms can change visual bounds without necessarily triggering
499
+ `ResizeObserver`.
500
+
501
+ Dynamic overlay content should use overlay-local state, component effects,
502
+ lifecycle callbacks feeding a subscription/external store, or another app-owned
503
+ update path rather than relying on `dragOverlay` being called for every pointer
504
+ move.
463
505
 
464
506
  ## Targeting And Modifiers
465
507
 
@@ -548,9 +590,9 @@ mount/unmount.
548
590
  `useDraggable`, `useDroppable`, and `useSortable` rely on React prop/ref updates
549
591
  for normal mount, unmount, and element replacement behavior.
550
592
 
551
- `useRemeasureDropTargets` is for layout changes, not resource release. The
552
- runtime also prunes disconnected targets on drag-critical paths such as
553
- remeasurement.
593
+ `useRemeasureDropTargets` and `useRemeasureOverlay` are for geometry changes,
594
+ not resource release. The runtime also prunes disconnected targets on
595
+ drag-critical paths such as remeasurement.
554
596
 
555
597
  ## Examples
556
598
 
@@ -584,6 +626,8 @@ React examples live in `apps/react-web/src/react`:
584
626
  - `useDragHandle()`: returns the handle attribute props for a handle element.
585
627
  - `useRemeasureDropTargets()`: returns a function for remeasuring all targets,
586
628
  one target, multiple targets, or a group.
629
+ - `useRemeasureOverlay()`: returns a function for manually refreshing the
630
+ currently mounted overlay measurement.
587
631
  - `composeRefs`: helper for combining the refs returned by hooks with app refs.
588
632
  - `lockToXAxis`, `lockToYAxis`: DOM movement modifiers re-exported by React.
589
633
  - `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 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,6 @@ 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 { useRemeasureOverlay } from "./hooks/use-remeasure-overlay.js";
11
12
  export { useSortable, type UseSortableOptions, type UseSortableResult, } from "./hooks/use-sortable.js";
12
13
  export { composeRefs } from "./utils/compose-refs.js";
package/dist/index.js CHANGED
@@ -6,5 +6,6 @@ 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 { useRemeasureOverlay } from "./hooks/use-remeasure-overlay.js";
9
10
  export { useSortable, } from "./hooks/use-sortable.js";
10
11
  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.3.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.3.0"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "react": "^19.0.0"