solidjs-motion 0.1.1 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -5,6 +5,49 @@ All notable changes to `solidjs-motion` / `@solidjs-motion/motion` are documente
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.3] — 2026-05-21
9
+
10
+ ### Fixed
11
+
12
+ - **`onDragEnd` now fires at the very end of pan-end** (after motion's
13
+ `whileDrag` flip, body-style restore, pointer-capture release, momentum
14
+ / snap-back dispatch, AND MV-ref cleanup) instead of mid-handler. A
15
+ synchronous state flip from a user `onDragEnd` callback — e.g. closing
16
+ a Kobalte / Radix-style Dialog whose contents are the draggable — used
17
+ to race motion's later DOM-touching work and could wedge surrounding
18
+ libraries that observe the same DOM (scroll lock, pointer-event
19
+ layer-stack). The new ordering closes the race: by the time the
20
+ callback runs, the drag session is fully torn down and any reactive
21
+ cascade triggered from it is unambiguous about ownership.
22
+
23
+ Observationally compatible with the previous behavior for callbacks
24
+ that don't flip global state; users can keep their callbacks as plain
25
+ synchronous functions and drop `queueMicrotask` workarounds.
26
+
27
+ ## [0.1.2] — 2026-05-20
28
+
29
+ ### Fixed
30
+
31
+ - **Drag now coexists with `initial` / `animate` / `exit`.** A draggable
32
+ element with an entrance animation (`<motion.X drag initial={{x:-300}}
33
+ animate={{x:0}}>`) used to break in two ways: animate's `x`/`y` never
34
+ reached the DOM, and on pointerdown the element snapped back to the
35
+ initial position. Three linked fixes:
36
+ - Drag's exclusion of `x`/`y` from the gesture-state winners is now
37
+ gated on `active.whileDrag` (pointer-engaged) instead of
38
+ `dragEnabled` (configured), matching motion-react. Animate's `x`/`y`
39
+ flow normally while drag is configured but idle; drag only claims
40
+ them during active interaction.
41
+ - The diff effect's removed-key fallback no longer reverts `x`/`y` to
42
+ `initial` when drag activates and excludes them. Drag *claiming* a
43
+ key is not the same as removing it.
44
+ - `handlePanStart` now syncs the x/y MotionValues to the element's
45
+ current visible translate (parsed from `getComputedStyle` +
46
+ `el.style.transform`) before capturing `dragStart`. motion's
47
+ `animate(el, target)` interpolates style.transform via WAAPI but
48
+ doesn't keep the visualElement's MVs in sync, so the MV would still
49
+ hold the entrance start value when drag began.
50
+
8
51
  ## [0.1.1] — 2026-05-20
9
52
 
10
53
  ### Docs
package/dist/index.js CHANGED
@@ -871,6 +871,91 @@ function ensureVisualElement(el) {
871
871
  return ve;
872
872
  }
873
873
  /**
874
+ * Parse the visible translate from an element's transform — reads both
875
+ * `getComputedStyle(el).transform` (which real browsers normalize to
876
+ * matrix form) AND `el.style.transform` (which preserves the raw syntax
877
+ * motion-dom's writer emits, e.g. `translate3d(50px, 0px, 0)`). Used by
878
+ * drag's pan-start to sync the x/y MotionValues to what the user is
879
+ * actually seeing.
880
+ *
881
+ * Why both sources: motion's `animate(el, target)` interpolates style.
882
+ * transform via WAAPI but DOESN'T update the VE's MVs during the tween,
883
+ * so after `initial: {x:-300} → animate: {x:0}` the MV still holds -300.
884
+ * Reading the current transform recovers the truth. We prefer computed
885
+ * (post-animation, post-WAAPI-commit value) and fall back to inline
886
+ * (covers jsdom + cases where motion's writer wrote inline but the
887
+ * browser hasn't run a style-resolve pass yet).
888
+ *
889
+ * Supported syntaxes:
890
+ * - `"none"` / empty → {0, 0}
891
+ * - `matrix(a, b, c, d, tx, ty)`
892
+ * - `matrix3d(..., tx, ty, ...)`
893
+ * - `translateX(Npx)` / `translateY(Npx)` / `translate(tx, ty)`
894
+ * - `translate3d(tx, ty, tz)`
895
+ * - Any of the above mixed with other transform functions (regex-
896
+ * based extraction picks just the translate components).
897
+ */
898
+ function readVisibleTranslate(el) {
899
+ const fromString = (transform) => {
900
+ if (!transform || transform === "none") return null;
901
+ if (transform.startsWith("matrix3d(")) {
902
+ const values = transform.slice(9, -1).split(",");
903
+ const tx = Number.parseFloat(values[12] ?? "0");
904
+ const ty = Number.parseFloat(values[13] ?? "0");
905
+ if (!Number.isFinite(tx) && !Number.isFinite(ty)) return null;
906
+ return {
907
+ x: Number.isFinite(tx) ? tx : 0,
908
+ y: Number.isFinite(ty) ? ty : 0
909
+ };
910
+ }
911
+ if (transform.startsWith("matrix(")) {
912
+ const values = transform.slice(7, -1).split(",");
913
+ const tx = Number.parseFloat(values[4] ?? "0");
914
+ const ty = Number.parseFloat(values[5] ?? "0");
915
+ if (!Number.isFinite(tx) && !Number.isFinite(ty)) return null;
916
+ return {
917
+ x: Number.isFinite(tx) ? tx : 0,
918
+ y: Number.isFinite(ty) ? ty : 0
919
+ };
920
+ }
921
+ let x = 0;
922
+ let y = 0;
923
+ let found = false;
924
+ const translate3d = transform.match(/translate3d\(\s*([-\d.]+)px\s*,\s*([-\d.]+)px/);
925
+ if (translate3d) {
926
+ x = Number.parseFloat(translate3d[1] ?? "0");
927
+ y = Number.parseFloat(translate3d[2] ?? "0");
928
+ found = true;
929
+ } else {
930
+ const translate2d = transform.match(/translate\(\s*([-\d.]+)px\s*(?:,\s*([-\d.]+)px)?/);
931
+ if (translate2d) {
932
+ x = Number.parseFloat(translate2d[1] ?? "0");
933
+ y = Number.parseFloat(translate2d[2] ?? "0");
934
+ found = true;
935
+ }
936
+ const translateX = transform.match(/translateX\(\s*([-\d.]+)px/);
937
+ if (translateX) {
938
+ x = Number.parseFloat(translateX[1] ?? "0");
939
+ found = true;
940
+ }
941
+ const translateY = transform.match(/translateY\(\s*([-\d.]+)px/);
942
+ if (translateY) {
943
+ y = Number.parseFloat(translateY[1] ?? "0");
944
+ found = true;
945
+ }
946
+ }
947
+ if (!found) return null;
948
+ return {
949
+ x: Number.isFinite(x) ? x : 0,
950
+ y: Number.isFinite(y) ? y : 0
951
+ };
952
+ };
953
+ return fromString(getComputedStyle(el).transform) ?? fromString(el.style.transform) ?? {
954
+ x: 0,
955
+ y: 0
956
+ };
957
+ }
958
+ /**
874
959
  * Compute the `touch-action` CSS value for an element being dragged.
875
960
  * Disabling touch-action prevents the browser from interpreting the gesture
876
961
  * as a scroll. Axis-locked drags leave the unused axis available for scroll
@@ -1008,14 +1093,27 @@ function createDrag(el, getOpts, setActive) {
1008
1093
  for (const ctrl of momentumControls) ctrl.stop();
1009
1094
  momentumControls = [];
1010
1095
  }
1011
- const handlePanStart = (event, info) => {
1096
+ const handlePanStart = (event, info, mvIsAuthoritative = false) => {
1012
1097
  if (!isDragEnabled()) return;
1013
1098
  stopMomentum();
1014
1099
  const ve = ensureVisualElement(el);
1015
1100
  xMV = ve.getValue("x", 0);
1016
1101
  yMV = ve.getValue("y", 0);
1017
- dragStartX = xMV.get();
1018
- dragStartY = yMV.get();
1102
+ const axis = getOpts().drag;
1103
+ if (mvIsAuthoritative) {
1104
+ dragStartX = xMV.get();
1105
+ dragStartY = yMV.get();
1106
+ } else {
1107
+ const visible = readVisibleTranslate(el);
1108
+ if (axis !== "y") {
1109
+ if (visible.x !== xMV.get()) xMV.set(visible.x);
1110
+ dragStartX = visible.x;
1111
+ } else dragStartX = xMV.get();
1112
+ if (axis !== "x") {
1113
+ if (visible.y !== yMV.get()) yMV.set(visible.y);
1114
+ dragStartY = visible.y;
1115
+ } else dragStartY = yMV.get();
1116
+ }
1019
1117
  sessionBounds = resolveConstraints(getOpts().dragConstraints, el, dragStartX, dragStartY);
1020
1118
  savedUserSelect = document.body.style.userSelect;
1021
1119
  savedTouchAction = el.style.touchAction;
@@ -1051,7 +1149,6 @@ function createDrag(el, getOpts, setActive) {
1051
1149
  setActive("whileDrag", false);
1052
1150
  restoreBodyAndElementStyles();
1053
1151
  releasePointerCaptureSafely();
1054
- getOpts().onDragEnd?.(event, info);
1055
1152
  const xRef = xMV;
1056
1153
  const yRef = yMV;
1057
1154
  const boundsRef = sessionBounds;
@@ -1147,6 +1244,7 @@ function createDrag(el, getOpts, setActive) {
1147
1244
  xMV = null;
1148
1245
  yMV = null;
1149
1246
  sessionBounds = null;
1247
+ getOpts().onDragEnd?.(event, info);
1150
1248
  };
1151
1249
  createPan(() => el, () => ({
1152
1250
  threshold: getOpts().panThreshold,
@@ -1184,7 +1282,7 @@ function createDrag(el, getOpts, setActive) {
1184
1282
  x: 0,
1185
1283
  y: 0
1186
1284
  }
1187
- });
1285
+ }, Boolean(options.snapToCursor));
1188
1286
  const sessionStartPoint = {
1189
1287
  x: event.clientX,
1190
1288
  y: event.clientY
@@ -1493,7 +1591,7 @@ function createGestureStateMachine(deps) {
1493
1591
  });
1494
1592
  const winners = createMemo(() => {
1495
1593
  const targets = stateTargets();
1496
- const dragEnabled = Boolean(getOpts().drag);
1594
+ const dragActive = active.whileDrag;
1497
1595
  const out = {};
1498
1596
  for (const stateName of PRIORITY_HIGH_TO_LOW) {
1499
1597
  if (!isStateActive(stateName, active, parentVariantCtx)) continue;
@@ -1502,7 +1600,7 @@ function createGestureStateMachine(deps) {
1502
1600
  for (const key in target) {
1503
1601
  if (key === "transition") continue;
1504
1602
  if (key in out) continue;
1505
- if (!active.exit && dragEnabled && (key === "x" || key === "y")) continue;
1603
+ if (!active.exit && dragActive && (key === "x" || key === "y")) continue;
1506
1604
  out[key] = {
1507
1605
  value: target[key],
1508
1606
  transition: target.transition,
@@ -1545,6 +1643,7 @@ function createGestureStateMachine(deps) {
1545
1643
  }
1546
1644
  for (const key in lastApplied) {
1547
1645
  if (key in next) continue;
1646
+ if (active.whileDrag && (key === "x" || key === "y")) continue;
1548
1647
  const initialValue = initialTarget && key in initialTarget ? initialTarget[key] : void 0;
1549
1648
  changes[key] = initialValue !== void 0 ? initialValue : getMotionDefault(key);
1550
1649
  }