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 +43 -0
- package/dist/index.js +106 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/primitives/createDrag.ts +138 -5
- package/src/primitives/gesture-state.ts +16 -5
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
|
-
|
|
1018
|
-
|
|
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
|
|
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 &&
|
|
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
|
}
|