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/package.json
CHANGED
|
@@ -77,6 +77,86 @@ function ensureVisualElement(el: HTMLElement): InstanceType<typeof HTMLVisualEle
|
|
|
77
77
|
return ve as InstanceType<typeof HTMLVisualElement>
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Parse the visible translate from an element's transform — reads both
|
|
82
|
+
* `getComputedStyle(el).transform` (which real browsers normalize to
|
|
83
|
+
* matrix form) AND `el.style.transform` (which preserves the raw syntax
|
|
84
|
+
* motion-dom's writer emits, e.g. `translate3d(50px, 0px, 0)`). Used by
|
|
85
|
+
* drag's pan-start to sync the x/y MotionValues to what the user is
|
|
86
|
+
* actually seeing.
|
|
87
|
+
*
|
|
88
|
+
* Why both sources: motion's `animate(el, target)` interpolates style.
|
|
89
|
+
* transform via WAAPI but DOESN'T update the VE's MVs during the tween,
|
|
90
|
+
* so after `initial: {x:-300} → animate: {x:0}` the MV still holds -300.
|
|
91
|
+
* Reading the current transform recovers the truth. We prefer computed
|
|
92
|
+
* (post-animation, post-WAAPI-commit value) and fall back to inline
|
|
93
|
+
* (covers jsdom + cases where motion's writer wrote inline but the
|
|
94
|
+
* browser hasn't run a style-resolve pass yet).
|
|
95
|
+
*
|
|
96
|
+
* Supported syntaxes:
|
|
97
|
+
* - `"none"` / empty → {0, 0}
|
|
98
|
+
* - `matrix(a, b, c, d, tx, ty)`
|
|
99
|
+
* - `matrix3d(..., tx, ty, ...)`
|
|
100
|
+
* - `translateX(Npx)` / `translateY(Npx)` / `translate(tx, ty)`
|
|
101
|
+
* - `translate3d(tx, ty, tz)`
|
|
102
|
+
* - Any of the above mixed with other transform functions (regex-
|
|
103
|
+
* based extraction picks just the translate components).
|
|
104
|
+
*/
|
|
105
|
+
function readVisibleTranslate(el: HTMLElement): { x: number; y: number } {
|
|
106
|
+
const fromString = (transform: string): { x: number; y: number } | null => {
|
|
107
|
+
if (!transform || transform === "none") return null
|
|
108
|
+
|
|
109
|
+
if (transform.startsWith("matrix3d(")) {
|
|
110
|
+
const values = transform.slice(9, -1).split(",")
|
|
111
|
+
const tx = Number.parseFloat(values[12] ?? "0")
|
|
112
|
+
const ty = Number.parseFloat(values[13] ?? "0")
|
|
113
|
+
if (!Number.isFinite(tx) && !Number.isFinite(ty)) return null
|
|
114
|
+
return { x: Number.isFinite(tx) ? tx : 0, y: Number.isFinite(ty) ? ty : 0 }
|
|
115
|
+
}
|
|
116
|
+
if (transform.startsWith("matrix(")) {
|
|
117
|
+
const values = transform.slice(7, -1).split(",")
|
|
118
|
+
const tx = Number.parseFloat(values[4] ?? "0")
|
|
119
|
+
const ty = Number.parseFloat(values[5] ?? "0")
|
|
120
|
+
if (!Number.isFinite(tx) && !Number.isFinite(ty)) return null
|
|
121
|
+
return { x: Number.isFinite(tx) ? tx : 0, y: Number.isFinite(ty) ? ty : 0 }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// motion-dom's writer emits the keyword form: `translate3d(...)`,
|
|
125
|
+
// `translateX(...)`, etc. — usually as the first segment of a
|
|
126
|
+
// composed transform like `translate3d(50px, 0px, 0) scale(1)`.
|
|
127
|
+
let x = 0
|
|
128
|
+
let y = 0
|
|
129
|
+
let found = false
|
|
130
|
+
const translate3d = transform.match(/translate3d\(\s*([-\d.]+)px\s*,\s*([-\d.]+)px/)
|
|
131
|
+
if (translate3d) {
|
|
132
|
+
x = Number.parseFloat(translate3d[1] ?? "0")
|
|
133
|
+
y = Number.parseFloat(translate3d[2] ?? "0")
|
|
134
|
+
found = true
|
|
135
|
+
} else {
|
|
136
|
+
const translate2d = transform.match(/translate\(\s*([-\d.]+)px\s*(?:,\s*([-\d.]+)px)?/)
|
|
137
|
+
if (translate2d) {
|
|
138
|
+
x = Number.parseFloat(translate2d[1] ?? "0")
|
|
139
|
+
y = Number.parseFloat(translate2d[2] ?? "0")
|
|
140
|
+
found = true
|
|
141
|
+
}
|
|
142
|
+
const translateX = transform.match(/translateX\(\s*([-\d.]+)px/)
|
|
143
|
+
if (translateX) {
|
|
144
|
+
x = Number.parseFloat(translateX[1] ?? "0")
|
|
145
|
+
found = true
|
|
146
|
+
}
|
|
147
|
+
const translateY = transform.match(/translateY\(\s*([-\d.]+)px/)
|
|
148
|
+
if (translateY) {
|
|
149
|
+
y = Number.parseFloat(translateY[1] ?? "0")
|
|
150
|
+
found = true
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!found) return null
|
|
154
|
+
return { x: Number.isFinite(x) ? x : 0, y: Number.isFinite(y) ? y : 0 }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return fromString(getComputedStyle(el).transform) ?? fromString(el.style.transform) ?? { x: 0, y: 0 }
|
|
158
|
+
}
|
|
159
|
+
|
|
80
160
|
/**
|
|
81
161
|
* Compute the `touch-action` CSS value for an element being dragged.
|
|
82
162
|
* Disabling touch-action prevents the browser from interpreting the gesture
|
|
@@ -272,7 +352,7 @@ export function createDrag(
|
|
|
272
352
|
// Stable handler references — they close over getOpts so reactive opts
|
|
273
353
|
// are read at event time. Hoisted out of the createPan call so the
|
|
274
354
|
// function-form options below doesn't re-allocate them per getOpts() call.
|
|
275
|
-
const handlePanStart = (event: PointerEvent, info: PanInfo) => {
|
|
355
|
+
const handlePanStart = (event: PointerEvent, info: PanInfo, mvIsAuthoritative = false) => {
|
|
276
356
|
// The pan session always fires onPanStart once movement crosses the
|
|
277
357
|
// threshold. Drag's enable check is here, not at the createPan-setup
|
|
278
358
|
// site, so toggling `drag` off mid-life immediately stops drag
|
|
@@ -287,8 +367,44 @@ export function createDrag(
|
|
|
287
367
|
const ve = ensureVisualElement(el)
|
|
288
368
|
xMV = ve.getValue("x", 0) as MotionValue<number>
|
|
289
369
|
yMV = ve.getValue("y", 0) as MotionValue<number>
|
|
290
|
-
|
|
291
|
-
|
|
370
|
+
|
|
371
|
+
// dragStart capture — two modes:
|
|
372
|
+
//
|
|
373
|
+
// (1) Default: sync the MV to the element's CURRENT visible translate
|
|
374
|
+
// before capturing. motion's `animate(el, target)` interpolates
|
|
375
|
+
// `style.transform` via WAAPI but DOESN'T update the
|
|
376
|
+
// visualElement's x/y MVs in lockstep, so after an entrance
|
|
377
|
+
// animation (e.g. `initial: {x:-300} → animate: {x:0}`) the MV
|
|
378
|
+
// would still hold the start value. Reading the painted transform
|
|
379
|
+
// recovers the truth and seeds dragStart correctly.
|
|
380
|
+
//
|
|
381
|
+
// (2) `mvIsAuthoritative=true` (e.g. dragControls.start with
|
|
382
|
+
// snapToCursor): the caller wrote the MV synchronously RIGHT
|
|
383
|
+
// before reaching us, but motion-dom's writer is frame-scheduled,
|
|
384
|
+
// so `el.style.transform` may not reflect that write yet. Trust
|
|
385
|
+
// the MV in this path — visible would be stale.
|
|
386
|
+
//
|
|
387
|
+
// Only the axis drag actually uses is touched — touching the locked
|
|
388
|
+
// axis would generate spurious MV writes that callers + tests notice.
|
|
389
|
+
const axis = getOpts().drag
|
|
390
|
+
if (mvIsAuthoritative) {
|
|
391
|
+
dragStartX = xMV.get()
|
|
392
|
+
dragStartY = yMV.get()
|
|
393
|
+
} else {
|
|
394
|
+
const visible = readVisibleTranslate(el)
|
|
395
|
+
if (axis !== "y") {
|
|
396
|
+
if (visible.x !== xMV.get()) xMV.set(visible.x)
|
|
397
|
+
dragStartX = visible.x
|
|
398
|
+
} else {
|
|
399
|
+
dragStartX = xMV.get()
|
|
400
|
+
}
|
|
401
|
+
if (axis !== "x") {
|
|
402
|
+
if (visible.y !== yMV.get()) yMV.set(visible.y)
|
|
403
|
+
dragStartY = visible.y
|
|
404
|
+
} else {
|
|
405
|
+
dragStartY = yMV.get()
|
|
406
|
+
}
|
|
407
|
+
}
|
|
292
408
|
|
|
293
409
|
// Resolve constraints once per session. Reading layout rects mid-drag
|
|
294
410
|
// would cost a forced reflow per pointermove; one read at drag-start
|
|
@@ -358,7 +474,16 @@ export function createDrag(
|
|
|
358
474
|
restoreBodyAndElementStyles()
|
|
359
475
|
releasePointerCaptureSafely()
|
|
360
476
|
|
|
361
|
-
|
|
477
|
+
// The user's `onDragEnd` callback fires at the END of this function
|
|
478
|
+
// (just before the early-out), not here. Rationale: a synchronous
|
|
479
|
+
// state flip from the callback (e.g. closing a Dialog whose contents
|
|
480
|
+
// are this draggable) used to race motion's own post-callback work
|
|
481
|
+
// — momentum dispatch, MV-ref cleanup — and could wedge surrounding
|
|
482
|
+
// libraries that observe the same DOM (scroll lock, pointer-event
|
|
483
|
+
// layers). Firing AFTER all motion DOM-touching work guarantees a
|
|
484
|
+
// clean handoff: by the time the callback runs, the drag session is
|
|
485
|
+
// fully torn down and any subsequent reactive cascade is unambiguous
|
|
486
|
+
// about ownership.
|
|
362
487
|
|
|
363
488
|
// Capture refs locally — the closure clears xMV/yMV below before the
|
|
364
489
|
// momentum promise can resolve, but the inertia animation needs stable
|
|
@@ -514,6 +639,9 @@ export function createDrag(
|
|
|
514
639
|
xMV = null
|
|
515
640
|
yMV = null
|
|
516
641
|
sessionBounds = null
|
|
642
|
+
|
|
643
|
+
// Callback fires last — see the note at the top of handlePanEnd.
|
|
644
|
+
getOpts().onDragEnd?.(event, info)
|
|
517
645
|
}
|
|
518
646
|
|
|
519
647
|
// Function-form options so createPan reads `panThreshold` reactively.
|
|
@@ -567,13 +695,18 @@ export function createDrag(
|
|
|
567
695
|
// Fire handlePanStart with a synthesized initial PanInfo. This
|
|
568
696
|
// initializes xMV/yMV/dragStartX/Y, resolves bounds, sets body styles,
|
|
569
697
|
// captures pointer, activates whileDrag, and fires onDragStart.
|
|
698
|
+
//
|
|
699
|
+
// `mvIsAuthoritative=true` when the snap path just wrote the MVs
|
|
700
|
+
// above — handlePanStart should read FROM the MV (not from
|
|
701
|
+
// getComputedStyle) because motion-dom's writer is frame-scheduled
|
|
702
|
+
// and `el.style.transform` may not yet reflect the snap write.
|
|
570
703
|
const initialInfo: PanInfo = {
|
|
571
704
|
point: { x: event.clientX, y: event.clientY },
|
|
572
705
|
delta: { x: 0, y: 0 },
|
|
573
706
|
offset: { x: 0, y: 0 },
|
|
574
707
|
velocity: { x: 0, y: 0 },
|
|
575
708
|
}
|
|
576
|
-
handlePanStart(event, initialInfo)
|
|
709
|
+
handlePanStart(event, initialInfo, Boolean(options.snapToCursor))
|
|
577
710
|
|
|
578
711
|
// Track the session locally — these would normally live inside
|
|
579
712
|
// createPan's closure. Velocity samples use motion-dom's `time.now()`
|
|
@@ -242,7 +242,8 @@ export function createGestureStateMachine(
|
|
|
242
242
|
// (Q4 — gesture inheritance through context). The first active state that
|
|
243
243
|
// defines a key claims it; lower-priority states are skipped for that key.
|
|
244
244
|
//
|
|
245
|
-
// Q5/C-lean exclusion:
|
|
245
|
+
// Q5/C-lean exclusion: while drag is ACTIVE (pointer engaged), `x` and `y`
|
|
246
|
+
// are owned by
|
|
246
247
|
// createDrag (it writes them to the VisualElement's MotionValues during
|
|
247
248
|
// pointer phase). Filter them out of the winners map so motion's animate
|
|
248
249
|
// (called from this effect) doesn't fight drag's writes. Other transform
|
|
@@ -257,7 +258,12 @@ export function createGestureStateMachine(
|
|
|
257
258
|
// exit's translation reaches DOM until that happens.
|
|
258
259
|
const winners = createMemo<Record<string, WinnerEntry>>(() => {
|
|
259
260
|
const targets = stateTargets()
|
|
260
|
-
|
|
261
|
+
// Drag claims x/y only while the user is ACTIVELY dragging (pointer
|
|
262
|
+
// engaged → `active.whileDrag === true`). When drag is merely
|
|
263
|
+
// configured-but-idle, initial/animate/exit and other states get
|
|
264
|
+
// normal access to x/y — matching motion-react. Reading from `active`
|
|
265
|
+
// tracks the store; the winners memo re-runs when whileDrag flips.
|
|
266
|
+
const dragActive = active.whileDrag
|
|
261
267
|
const out: Record<string, WinnerEntry> = {}
|
|
262
268
|
for (const stateName of PRIORITY_HIGH_TO_LOW) {
|
|
263
269
|
if (!isStateActive(stateName, active, parentVariantCtx)) continue
|
|
@@ -268,9 +274,9 @@ export function createGestureStateMachine(
|
|
|
268
274
|
if (key === "transition") continue
|
|
269
275
|
// Higher-priority state already won this key.
|
|
270
276
|
if (key in out) continue
|
|
271
|
-
// x/y are drag-owned
|
|
272
|
-
//
|
|
273
|
-
if (!active.exit &&
|
|
277
|
+
// x/y are drag-owned during active drag — unless exit is also
|
|
278
|
+
// active, in which case exit's translation wins.
|
|
279
|
+
if (!active.exit && dragActive && (key === "x" || key === "y")) continue
|
|
274
280
|
out[key] = {
|
|
275
281
|
value: (target as Record<string, unknown>)[key],
|
|
276
282
|
transition: target.transition,
|
|
@@ -363,6 +369,11 @@ export function createGestureStateMachine(
|
|
|
363
369
|
}
|
|
364
370
|
for (const key in lastApplied) {
|
|
365
371
|
if (key in next) continue
|
|
372
|
+
// x/y aren't "removed" when drag is active — drag is CLAIMING them.
|
|
373
|
+
// Falling back to initial here would dispatch animate(el, {x:-W})
|
|
374
|
+
// on pointerdown and snap the element back to its initial state
|
|
375
|
+
// before the user's first move could reach the DOM.
|
|
376
|
+
if (active.whileDrag && (key === "x" || key === "y")) continue
|
|
366
377
|
// Removed-key fallback: own initial → motion default → null.
|
|
367
378
|
const initialValue =
|
|
368
379
|
initialTarget && key in (initialTarget as Record<string, unknown>)
|