@humanspeak/svelte-motion 0.5.1 → 0.5.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/dist/components/projection.context.d.ts +22 -0
- package/dist/components/projection.context.js +56 -0
- package/dist/html/_MotionContainer.svelte +328 -24
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +78 -0
- package/dist/utils/drag.d.ts +42 -11
- package/dist/utils/drag.js +103 -12
- package/dist/utils/layout.d.ts +26 -2
- package/dist/utils/layout.js +13 -2
- package/dist/utils/pan.d.ts +135 -0
- package/dist/utils/pan.js +426 -0
- package/dist/utils/projection.d.ts +287 -0
- package/dist/utils/projection.js +392 -0
- package/dist/utils/style.d.ts +23 -0
- package/dist/utils/style.js +27 -0
- package/dist/utils/variants.d.ts +26 -0
- package/dist/utils/variants.js +42 -0
- package/package.json +5 -5
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ProjectionNode } from '../utils/projection';
|
|
2
|
+
/**
|
|
3
|
+
* Publish a `ProjectionNode` as the projection parent for descendant
|
|
4
|
+
* motion components.
|
|
5
|
+
*
|
|
6
|
+
* @param node The current component's ProjectionNode, or `null` to
|
|
7
|
+
* explicitly clear (rarely needed — Svelte context auto-clears on
|
|
8
|
+
* component unmount).
|
|
9
|
+
*/
|
|
10
|
+
export declare const setProjectionParent: (node: ProjectionNode | null) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Read the ancestor `ProjectionNode` published by the nearest motion
|
|
13
|
+
* ancestor. Returns `null` when this component is at the root of the
|
|
14
|
+
* projection tree (or when no motion ancestor exists).
|
|
15
|
+
*
|
|
16
|
+
* Must be called BEFORE the current component publishes its own node
|
|
17
|
+
* via `setProjectionParent` — see the order-of-operations note in this
|
|
18
|
+
* file's header.
|
|
19
|
+
*
|
|
20
|
+
* @returns The parent ProjectionNode, or `null`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const getProjectionParent: () => ProjectionNode | null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Svelte context plumbing for the projection tree.
|
|
4
|
+
*
|
|
5
|
+
* Every `motion.*` element creates a `ProjectionNode` at setup time and
|
|
6
|
+
* publishes it via `setProjectionParent(node)` so descendant motion
|
|
7
|
+
* elements can pick it up via `getProjectionParent()` and wire themselves
|
|
8
|
+
* as `node.parent` + register in `node.parent.children`. The tree shape
|
|
9
|
+
* mirrors the Svelte component tree exactly because Svelte's
|
|
10
|
+
* `setContext` propagates down through descendants in component-init
|
|
11
|
+
* order.
|
|
12
|
+
*
|
|
13
|
+
* This is the closest analog we have to framer-motion's depth-sorted
|
|
14
|
+
* FlatTree (`projection/node/create-projection-node.ts`), but without
|
|
15
|
+
* the global `root` registry — parent/child pointers are sufficient
|
|
16
|
+
* for the workflows this PR enables (drag↔swap origin compensation,
|
|
17
|
+
* ancestor-zeroing measure). A global root would only be needed once we
|
|
18
|
+
* port shared-element `layoutId` morphing onto projection nodes; the
|
|
19
|
+
* current `layoutId.ts` registry continues to handle that case
|
|
20
|
+
* independently.
|
|
21
|
+
*
|
|
22
|
+
* Order of operations inside a single `motion.*` component (CRITICAL):
|
|
23
|
+
* 1. Read parent via `getProjectionParent()` BEFORE creating own node.
|
|
24
|
+
* 2. Construct own node, set `node.parent = parent`.
|
|
25
|
+
* 3. Publish own node via `setProjectionParent(node)`.
|
|
26
|
+
* If steps 1 and 3 are reversed, the component sees ITS OWN node as
|
|
27
|
+
* the parent (because `setContext` shadows from the call site down)
|
|
28
|
+
* and the tree collapses to a chain of self-references. Same trap
|
|
29
|
+
* `layoutScroll.context.ts` documents — don't repeat it.
|
|
30
|
+
*/
|
|
31
|
+
const PROJECTION_CONTEXT_KEY = Symbol('svelte-motion:projection-parent');
|
|
32
|
+
/**
|
|
33
|
+
* Publish a `ProjectionNode` as the projection parent for descendant
|
|
34
|
+
* motion components.
|
|
35
|
+
*
|
|
36
|
+
* @param node The current component's ProjectionNode, or `null` to
|
|
37
|
+
* explicitly clear (rarely needed — Svelte context auto-clears on
|
|
38
|
+
* component unmount).
|
|
39
|
+
*/
|
|
40
|
+
export const setProjectionParent = (node) => {
|
|
41
|
+
setContext(PROJECTION_CONTEXT_KEY, node);
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Read the ancestor `ProjectionNode` published by the nearest motion
|
|
45
|
+
* ancestor. Returns `null` when this component is at the root of the
|
|
46
|
+
* projection tree (or when no motion ancestor exists).
|
|
47
|
+
*
|
|
48
|
+
* Must be called BEFORE the current component publishes its own node
|
|
49
|
+
* via `setProjectionParent` — see the order-of-operations note in this
|
|
50
|
+
* file's header.
|
|
51
|
+
*
|
|
52
|
+
* @returns The parent ProjectionNode, or `null`.
|
|
53
|
+
*/
|
|
54
|
+
export const getProjectionParent = () => {
|
|
55
|
+
return getContext(PROJECTION_CONTEXT_KEY) ?? null;
|
|
56
|
+
};
|
|
@@ -17,17 +17,21 @@
|
|
|
17
17
|
DragControls,
|
|
18
18
|
DragTransition,
|
|
19
19
|
MotionWhileDrag,
|
|
20
|
-
DragInfo
|
|
20
|
+
DragInfo,
|
|
21
|
+
MotionOnPan,
|
|
22
|
+
MotionOnPanEnd,
|
|
23
|
+
MotionOnPanSessionStart,
|
|
24
|
+
MotionOnPanStart
|
|
21
25
|
} from '../types'
|
|
22
26
|
import { isNotEmpty } from '../utils/objects'
|
|
23
27
|
import { sleep } from '../utils/testing'
|
|
24
28
|
import { animate, type AnimationOptions, type DOMKeyframesDefinition } from 'motion'
|
|
25
29
|
import { isPlaywrightEnv, pwLog } from '../utils/log'
|
|
26
|
-
import { onDestroy, type Snippet } from 'svelte'
|
|
30
|
+
import { onDestroy, untrack, type Snippet } from 'svelte'
|
|
27
31
|
import { VOID_TAGS } from '../utils/constants'
|
|
28
32
|
import { mergeTransitions, animateWithLifecycle } from '../utils/animation'
|
|
29
33
|
import { attachWhileTap } from '../utils/interaction'
|
|
30
|
-
import { attachWhileHover } from '../utils/hover'
|
|
34
|
+
import { attachWhileHover, computeHoverBaseline, splitHoverDefinition } from '../utils/hover'
|
|
31
35
|
import { attachWhileFocus } from '../utils/focus'
|
|
32
36
|
import { attachWhileInView } from '../utils/inView.svelte'
|
|
33
37
|
import {
|
|
@@ -35,10 +39,11 @@
|
|
|
35
39
|
computeFlipTransforms,
|
|
36
40
|
runFlipAnimation,
|
|
37
41
|
setCompositorHints,
|
|
38
|
-
observeLayoutChanges
|
|
42
|
+
observeLayoutChanges,
|
|
43
|
+
type RectLike
|
|
39
44
|
} from '../utils/layout'
|
|
40
45
|
import type { SvelteHTMLElements } from 'svelte/elements'
|
|
41
|
-
import { mergeInlineStyles } from '../utils/style'
|
|
46
|
+
import { mergeInlineStyles, extractTransform } from '../utils/style'
|
|
42
47
|
import { isNativelyFocusable } from '../utils/a11y'
|
|
43
48
|
import {
|
|
44
49
|
getAnimatePresenceContext,
|
|
@@ -48,7 +53,16 @@
|
|
|
48
53
|
} from '../utils/presence'
|
|
49
54
|
import { getInitialKeyframes } from '../utils/initial'
|
|
50
55
|
import { attachDrag } from '../utils/drag'
|
|
51
|
-
import {
|
|
56
|
+
import { attachPan, type AttachPanCleanup } from '../utils/pan'
|
|
57
|
+
import { ProjectionNode } from '../utils/projection'
|
|
58
|
+
import { getProjectionParent, setProjectionParent } from '../components/projection.context'
|
|
59
|
+
import {
|
|
60
|
+
resolveInitial,
|
|
61
|
+
resolveAnimate,
|
|
62
|
+
resolveExit,
|
|
63
|
+
resolveWhile,
|
|
64
|
+
resolveRestingValues
|
|
65
|
+
} from '../utils/variants'
|
|
52
66
|
import {
|
|
53
67
|
setVariantContext,
|
|
54
68
|
getVariantContext,
|
|
@@ -97,6 +111,11 @@
|
|
|
97
111
|
whileInView: whileInViewProp,
|
|
98
112
|
viewport: viewportProp,
|
|
99
113
|
whileDrag: whileDragProp,
|
|
114
|
+
whilePan: whilePanProp,
|
|
115
|
+
onPanSessionStart: onPanSessionStartProp,
|
|
116
|
+
onPanStart: onPanStartProp,
|
|
117
|
+
onPan: onPanProp,
|
|
118
|
+
onPanEnd: onPanEndProp,
|
|
100
119
|
onHoverStart: onHoverStartProp,
|
|
101
120
|
onHoverEnd: onHoverEndProp,
|
|
102
121
|
onFocusStart: onFocusStartProp,
|
|
@@ -124,10 +143,17 @@
|
|
|
124
143
|
layout: layoutProp,
|
|
125
144
|
layoutId: layoutIdProp,
|
|
126
145
|
layoutScroll: layoutScrollProp,
|
|
146
|
+
onProjectionUpdate: onProjectionUpdateProp,
|
|
127
147
|
ref: element = $bindable(null),
|
|
128
148
|
...rest
|
|
129
149
|
}: Props = $props()
|
|
130
150
|
let isLoaded = $state<'mounting' | 'initial' | 'ready' | 'animated'>('mounting')
|
|
151
|
+
// True once the enter/animate animation has COMPLETED. Until then the
|
|
152
|
+
// WAAPI animation owns the transform; flipping the inline baseline to
|
|
153
|
+
// the target mid-run causes a one-frame snap (the target shows through
|
|
154
|
+
// for the frame the inline changes). We therefore only apply the target
|
|
155
|
+
// as the inline style once settled — see the style derivation. (#377)
|
|
156
|
+
let enterAnimationSettled = $state(false)
|
|
131
157
|
let dataPath = $state<number>(-1)
|
|
132
158
|
const motionConfig = $derived(getMotionConfig())
|
|
133
159
|
const reducedMotionState = useReducedMotionConfig()
|
|
@@ -181,6 +207,46 @@
|
|
|
181
207
|
return refs.filter((el): el is HTMLElement => Boolean(el))
|
|
182
208
|
}
|
|
183
209
|
|
|
210
|
+
// Projection tree wiring (#379). Capture the parent node BEFORE
|
|
211
|
+
// publishing our own — same shadowing trap as layoutScroll above.
|
|
212
|
+
// The node measures through `resolveLayoutScrollAncestors` so its
|
|
213
|
+
// boxes share the FLIP coordinate space, and zeros ancestor
|
|
214
|
+
// transforms during measure so nested layout-animated parents don't
|
|
215
|
+
// corrupt a child's delta.
|
|
216
|
+
// The user-authored transform, sourced from the `style` prop rather
|
|
217
|
+
// than the live inline transform — the latter already carries any
|
|
218
|
+
// transform-type `initial`/`animate` keyframe by the time the node
|
|
219
|
+
// measures, which would be mistaken for the user's base.
|
|
220
|
+
const userBaseTransform = $derived(extractTransform(styleProp))
|
|
221
|
+
|
|
222
|
+
const projectionParent = getProjectionParent()
|
|
223
|
+
const projection = new ProjectionNode({
|
|
224
|
+
parent: projectionParent,
|
|
225
|
+
getScrollContainers: resolveLayoutScrollAncestors,
|
|
226
|
+
getBaseTransform: () => userBaseTransform
|
|
227
|
+
})
|
|
228
|
+
setProjectionParent(projection)
|
|
229
|
+
|
|
230
|
+
// Convert a projection `Box` (ancestor chain reset to base, self
|
|
231
|
+
// transform stripped, scroll containers compensated) to the
|
|
232
|
+
// `RectLike` shape `computeFlipTransforms` consumes.
|
|
233
|
+
const boxToRectLike = (box: {
|
|
234
|
+
x: { min: number; max: number }
|
|
235
|
+
y: { min: number; max: number }
|
|
236
|
+
}): RectLike => ({
|
|
237
|
+
left: box.x.min,
|
|
238
|
+
top: box.y.min,
|
|
239
|
+
width: box.x.max - box.x.min,
|
|
240
|
+
height: box.y.max - box.y.min
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// Ancestor-transform-invariant layout measurement for seeding the
|
|
244
|
+
// FLIP effect's first rect.
|
|
245
|
+
const measureLayoutRect = (): RectLike | null => {
|
|
246
|
+
const box = projection.measure()
|
|
247
|
+
return box ? boxToRectLike(box) : null
|
|
248
|
+
}
|
|
249
|
+
|
|
184
250
|
// Get current presence depth (0 = direct child of AnimatePresence, undefined = not in AnimatePresence)
|
|
185
251
|
const presenceDepth = getPresenceDepth()
|
|
186
252
|
|
|
@@ -455,6 +521,7 @@
|
|
|
455
521
|
const resolvedWhileHover = $derived(resolveWhile(whileHoverProp, variantsProp, effectiveCustom))
|
|
456
522
|
const resolvedWhileFocus = $derived(resolveWhile(whileFocusProp, variantsProp, effectiveCustom))
|
|
457
523
|
const resolvedWhileDrag = $derived(resolveWhile(whileDragProp, variantsProp, effectiveCustom))
|
|
524
|
+
const resolvedWhilePan = $derived(resolveWhile(whilePanProp, variantsProp, effectiveCustom))
|
|
458
525
|
const resolvedWhileInView = $derived(
|
|
459
526
|
resolveWhile(whileInViewProp, variantsProp, effectiveCustom)
|
|
460
527
|
)
|
|
@@ -467,6 +534,17 @@
|
|
|
467
534
|
)
|
|
468
535
|
)
|
|
469
536
|
|
|
537
|
+
// Reduced-motion-filtered animate values used as the inline-style
|
|
538
|
+
// baseline so an animated value (transforms included) persists after
|
|
539
|
+
// the WAAPI animation completes (#377). Filtered exactly like
|
|
540
|
+
// `initialKeyframes` so transforms are stripped under reduced motion.
|
|
541
|
+
const animateKeyframes = $derived(
|
|
542
|
+
filterReducedMotionKeyframes(
|
|
543
|
+
resolvedAnimate as Record<string, unknown> | undefined,
|
|
544
|
+
reducedMotion
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
|
|
470
548
|
// Derived attributes to keep both branches in sync (focusability, data flags, style, class)
|
|
471
549
|
const derivedAttrs = $derived<Record<string, unknown>>({
|
|
472
550
|
...(rest as Record<string, unknown>),
|
|
@@ -505,20 +583,33 @@
|
|
|
505
583
|
initialKeyframes && 'pathLength' in initialKeyframes && isLoaded === 'mounting'
|
|
506
584
|
? `${styleProp || ''};visibility:hidden`
|
|
507
585
|
: styleProp,
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
586
|
+
// The "from" slot: apply initialKeyframes as inline styles during
|
|
587
|
+
// the mounting/initial phases (before the WAAPI animation locks
|
|
588
|
+
// its from-value and we promote to 'ready' — see the lifecycle
|
|
589
|
+
// around the enter rAF). mergeInlineStyles prefers this slot when
|
|
590
|
+
// non-empty, so it wins over the animate slot below in these phases.
|
|
512
591
|
isLoaded === 'mounting' || isLoaded === 'initial'
|
|
513
592
|
? (initialKeyframes as unknown as Record<string, unknown>)
|
|
514
593
|
: undefined,
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
594
|
+
// The "target" slot. Only AFTER the enter animation completes does
|
|
595
|
+
// the target become the inline baseline, so the element holds it
|
|
596
|
+
// once WAAPI surrenders the property (default fill:'none' would
|
|
597
|
+
// otherwise leave transform:none). It must NOT be applied during
|
|
598
|
+
// the run: flipping the inline value to the target mid-animation
|
|
599
|
+
// shows the target for the one frame the inline changes (a visible
|
|
600
|
+
// snap), since WAAPI's composite doesn't override that exact frame.
|
|
601
|
+
// While the animation runs we keep the original behavior — initial
|
|
602
|
+
// keyframes own the inline (via the slot above), or, with no
|
|
603
|
+
// initial, the animate values seed the inline as the from. Resting
|
|
604
|
+
// values collapse keyframe arrays to their last element
|
|
605
|
+
// (animate={{x:[0,100,50]}} rests at 50). (#377)
|
|
606
|
+
enterAnimationSettled
|
|
607
|
+
? (resolveRestingValues(
|
|
608
|
+
animateKeyframes as DOMKeyframesDefinition | undefined
|
|
609
|
+
) as unknown as Record<string, unknown>)
|
|
610
|
+
: isNotEmpty(initialKeyframes)
|
|
611
|
+
? undefined
|
|
612
|
+
: (animateKeyframes as unknown as Record<string, unknown>)
|
|
522
613
|
),
|
|
523
614
|
class: classProp
|
|
524
615
|
})
|
|
@@ -594,6 +685,172 @@
|
|
|
594
685
|
}
|
|
595
686
|
})
|
|
596
687
|
|
|
688
|
+
/**
|
|
689
|
+
* Pan-gesture wiring. Active whenever any of `onPanSessionStart`,
|
|
690
|
+
* `onPanStart`, `onPan`, `onPanEnd`, or `whilePan` is set. Unlike
|
|
691
|
+
* `drag`, Pan has no constraints / momentum / origin-snap — it's a
|
|
692
|
+
* pure pointer offset+velocity reporter, useful for swipe-to-dismiss
|
|
693
|
+
* sheets, custom carousels, and any "tell me what the gesture is
|
|
694
|
+
* doing right now" interaction. Mirrors framer-motion's `PanGesture`
|
|
695
|
+
* (packages/framer-motion/src/gestures/pan/index.ts).
|
|
696
|
+
*
|
|
697
|
+
* Split into TWO effects:
|
|
698
|
+
*
|
|
699
|
+
* 1. `attach` — keyed on `element`, `isLoaded === 'ready'`, presence
|
|
700
|
+
* of any pan handler/whilePan, and absence of `drag` (drag takes
|
|
701
|
+
* precedence — upstream framer-motion routes drag THROUGH the pan
|
|
702
|
+
* gesture internally, so co-attaching pan when drag is on would
|
|
703
|
+
* fight transforms). Creates / tears down the underlying
|
|
704
|
+
* `attachPan` lifetime once per element-bound interval.
|
|
705
|
+
*
|
|
706
|
+
* 2. `swap` — keyed on the user's handler/whilePan props. Calls
|
|
707
|
+
* `teardownPan.update(next)` to hot-swap the live handler set
|
|
708
|
+
* without destroying the in-flight `PanSession`. Without this
|
|
709
|
+
* split, every parent re-render that produces a fresh inline
|
|
710
|
+
* arrow handler would tear down the live gesture mid-pan —
|
|
711
|
+
* pointer listeners removed, no `onPanEnd` ever fires, whilePan
|
|
712
|
+
* keyframes leak.
|
|
713
|
+
*/
|
|
714
|
+
let teardownPan: AttachPanCleanup | null = null
|
|
715
|
+
let activeWhilePanKeyframes: Record<string, unknown> | null = null
|
|
716
|
+
let whilePanBaseline: Record<string, unknown> | null = null
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Boolean presence-check for "is any pan surface active?". Derived
|
|
720
|
+
* so the attach effect below tracks the *boolean value*, not the
|
|
721
|
+
* individual handler/whilePan reference identities. A consumer
|
|
722
|
+
* passing `onPan={(e, i) => ...}` (inline arrow — fresh ref every
|
|
723
|
+
* render) used to re-trigger the attach effect on every parent
|
|
724
|
+
* render; with this derived in place, the attach effect only
|
|
725
|
+
* re-runs when overall presence flips (none → some, some → none).
|
|
726
|
+
* Per-ref changes flow through the hot-swap effect instead.
|
|
727
|
+
*/
|
|
728
|
+
const hasAnyPanHandler = $derived(
|
|
729
|
+
!!onPanProp ||
|
|
730
|
+
!!onPanStartProp ||
|
|
731
|
+
!!onPanEndProp ||
|
|
732
|
+
!!onPanSessionStartProp ||
|
|
733
|
+
!!resolvedWhilePan
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
const buildPanHandlers = (): {
|
|
737
|
+
onSessionStart?: MotionOnPanSessionStart
|
|
738
|
+
onStart: NonNullable<MotionOnPanStart>
|
|
739
|
+
onMove?: MotionOnPan
|
|
740
|
+
onEnd: NonNullable<MotionOnPanEnd>
|
|
741
|
+
} => ({
|
|
742
|
+
onSessionStart: onPanSessionStartProp,
|
|
743
|
+
onStart: (event, info) => {
|
|
744
|
+
if (resolvedWhilePan && element) {
|
|
745
|
+
// Snapshot the values we'll revert to BEFORE applying — same
|
|
746
|
+
// `computeHoverBaseline` path the other while-* gestures
|
|
747
|
+
// (whileHover/whileFocus/drag) use. Covers animatable transform
|
|
748
|
+
// shorthands (scale, rotate, x, y) AND restores non-animatable
|
|
749
|
+
// inline writes (cursor, pointer-events) since the baseline
|
|
750
|
+
// sniffs `animate` → `initial` → computed style → inline style.
|
|
751
|
+
whilePanBaseline = computeHoverBaseline(element, {
|
|
752
|
+
initial: (initialKeyframes ?? {}) as Record<string, unknown>,
|
|
753
|
+
animate: (resolvedAnimate ?? {}) as Record<string, unknown>,
|
|
754
|
+
whileHover: (resolvedWhilePan ?? {}) as Record<string, unknown>
|
|
755
|
+
})
|
|
756
|
+
const { keyframes, transition } = splitHoverDefinition(
|
|
757
|
+
resolvedWhilePan as Record<string, unknown>
|
|
758
|
+
)
|
|
759
|
+
activeWhilePanKeyframes = keyframes
|
|
760
|
+
animateWithLifecycle(
|
|
761
|
+
element,
|
|
762
|
+
keyframes as unknown as DOMKeyframesDefinition,
|
|
763
|
+
(transition ?? mergedTransition ?? {}) as unknown as AnimationOptions
|
|
764
|
+
)
|
|
765
|
+
}
|
|
766
|
+
onPanStartProp?.(event, info)
|
|
767
|
+
},
|
|
768
|
+
onMove: onPanProp,
|
|
769
|
+
onEnd: (event, info) => {
|
|
770
|
+
if (activeWhilePanKeyframes && whilePanBaseline && element) {
|
|
771
|
+
animateWithLifecycle(
|
|
772
|
+
element,
|
|
773
|
+
whilePanBaseline as unknown as DOMKeyframesDefinition,
|
|
774
|
+
(mergedTransition ?? {}) as unknown as AnimationOptions
|
|
775
|
+
)
|
|
776
|
+
}
|
|
777
|
+
activeWhilePanKeyframes = null
|
|
778
|
+
whilePanBaseline = null
|
|
779
|
+
onPanEndProp?.(event, info)
|
|
780
|
+
}
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
$effect(() => {
|
|
784
|
+
if (isPlaywright) {
|
|
785
|
+
pwLog('[motion] pan attach effect run', {
|
|
786
|
+
hasAnyPanHandler,
|
|
787
|
+
isLoaded
|
|
788
|
+
})
|
|
789
|
+
}
|
|
790
|
+
if (!element) return
|
|
791
|
+
// Defer attachment until the element has settled out of the enter
|
|
792
|
+
// animation phase — matches the gate every other gesture effect
|
|
793
|
+
// in this file uses (drag, whileTap, whileHover, whileFocus,
|
|
794
|
+
// whileInView). Without this, a pointerdown during the
|
|
795
|
+
// initial / mounting phase would attach pan listeners against an
|
|
796
|
+
// element whose enter animation hasn't committed its baseline.
|
|
797
|
+
if (isLoaded !== 'ready') return
|
|
798
|
+
// Drag takes precedence — upstream framer-motion's drag gesture is
|
|
799
|
+
// implemented ON TOP of Pan, not alongside it. Co-attaching here
|
|
800
|
+
// would create two competing pointer pipelines fighting for the
|
|
801
|
+
// same transforms.
|
|
802
|
+
if (dragProp) return
|
|
803
|
+
if (!hasAnyPanHandler) return
|
|
804
|
+
|
|
805
|
+
// `untrack` so the reactive reads inside `buildPanHandlers`
|
|
806
|
+
// (onPan*Prop, resolvedWhilePan, initialKeyframes, resolvedAnimate,
|
|
807
|
+
// mergedTransition) don't register as dependencies of this attach
|
|
808
|
+
// effect. Otherwise every parent re-render that passes a fresh
|
|
809
|
+
// inline arrow handler would re-run this effect and call
|
|
810
|
+
// `teardownPan?.()`, killing the live PanSession mid-gesture.
|
|
811
|
+
// Handler-ref changes flow exclusively through the hot-swap
|
|
812
|
+
// effect below, which calls `teardownPan.update(next)` — that's
|
|
813
|
+
// the path that keeps an in-flight gesture alive across re-renders.
|
|
814
|
+
teardownPan = attachPan(
|
|
815
|
+
element,
|
|
816
|
+
untrack(() => buildPanHandlers())
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
return () => {
|
|
820
|
+
// Synchronous revert of whilePan + lifecycle dispatch lives in
|
|
821
|
+
// attachPan.teardown() — the cleanup chain there calls
|
|
822
|
+
// session.dispatchTerminal(rawHandlers) BEFORE flipping isAlive,
|
|
823
|
+
// so onPanEnd fires (which runs the revert above) before the
|
|
824
|
+
// listeners go. dispatchTerminal is idempotent (PanSession's
|
|
825
|
+
// terminalDispatched flag) so a host that tears down after a
|
|
826
|
+
// natural release won't replay the lifecycle pair.
|
|
827
|
+
teardownPan?.()
|
|
828
|
+
teardownPan = null
|
|
829
|
+
activeWhilePanKeyframes = null
|
|
830
|
+
whilePanBaseline = null
|
|
831
|
+
}
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Hot-swap effect — propagates handler / whilePan changes onto the
|
|
836
|
+
* existing PanSession via `teardownPan.update(next)`. Tracked
|
|
837
|
+
* separately from the attach effect so a fresh inline-arrow handler
|
|
838
|
+
* reference does NOT trigger teardown + re-attach. Without this
|
|
839
|
+
* split, every parent re-render mid-gesture would silently kill the
|
|
840
|
+
* live pan session.
|
|
841
|
+
*/
|
|
842
|
+
$effect(() => {
|
|
843
|
+
// Track every prop the handler set depends on so this effect
|
|
844
|
+
// re-runs when any of them change.
|
|
845
|
+
void onPanSessionStartProp
|
|
846
|
+
void onPanStartProp
|
|
847
|
+
void onPanProp
|
|
848
|
+
void onPanEndProp
|
|
849
|
+
void resolvedWhilePan
|
|
850
|
+
if (!teardownPan) return
|
|
851
|
+
teardownPan.update(buildPanHandlers())
|
|
852
|
+
})
|
|
853
|
+
|
|
597
854
|
/**
|
|
598
855
|
* Execute the actual animation without wait mode checks.
|
|
599
856
|
*/
|
|
@@ -630,12 +887,20 @@
|
|
|
630
887
|
transitionAnimate
|
|
631
888
|
})
|
|
632
889
|
|
|
890
|
+
// A fresh run owns the transform again until it completes.
|
|
891
|
+
enterAnimationSettled = false
|
|
633
892
|
animateWithLifecycle(
|
|
634
893
|
element,
|
|
635
894
|
payload as unknown as DOMKeyframesDefinition,
|
|
636
895
|
transitionAnimate as unknown as AnimationOptions,
|
|
637
896
|
(def) => onAnimationStartProp?.(def as unknown as DOMKeyframesDefinition | undefined),
|
|
638
|
-
(def) =>
|
|
897
|
+
(def) => {
|
|
898
|
+
// Now the target is the resting state — promote it to the
|
|
899
|
+
// inline baseline so it persists after WAAPI surrenders the
|
|
900
|
+
// property (default fill:'none'). (#377)
|
|
901
|
+
enterAnimationSettled = true
|
|
902
|
+
onAnimationCompleteProp?.(def as unknown as DOMKeyframesDefinition | undefined)
|
|
903
|
+
}
|
|
639
904
|
)
|
|
640
905
|
}
|
|
641
906
|
|
|
@@ -753,26 +1018,65 @@
|
|
|
753
1018
|
: undefined
|
|
754
1019
|
)
|
|
755
1020
|
|
|
1021
|
+
// Projection node lifecycle + the `onProjectionUpdate` listener.
|
|
1022
|
+
// Mount once the element binds; seed the baseline layout; unmount on
|
|
1023
|
+
// cleanup. Depends ONLY on `element` — the `onProjectionUpdate`
|
|
1024
|
+
// subscription lives in its own effect below so a change to the
|
|
1025
|
+
// callback's identity re-subscribes WITHOUT tearing the node down
|
|
1026
|
+
// (an unmount would clear latestLayout/children and re-seed the
|
|
1027
|
+
// first commit instead of emitting a real delta).
|
|
1028
|
+
$effect(() => {
|
|
1029
|
+
if (!element) return
|
|
1030
|
+
projection.mount(element)
|
|
1031
|
+
projection.measure() // seed latestLayout so the first commit can diff
|
|
1032
|
+
return () => {
|
|
1033
|
+
projection.unmount()
|
|
1034
|
+
}
|
|
1035
|
+
})
|
|
1036
|
+
|
|
1037
|
+
// Subscribe the consumer's `onProjectionUpdate` callback. Separate
|
|
1038
|
+
// from the mount effect so re-subscribing on a callback-identity
|
|
1039
|
+
// change never unmounts the node.
|
|
1040
|
+
$effect(() => {
|
|
1041
|
+
if (!(element && onProjectionUpdateProp)) return
|
|
1042
|
+
const off = projection.addEventListener('didUpdate', (data) => onProjectionUpdateProp(data))
|
|
1043
|
+
return () => {
|
|
1044
|
+
off()
|
|
1045
|
+
}
|
|
1046
|
+
})
|
|
1047
|
+
|
|
756
1048
|
// Minimal layout animation using FLIP when `layout` is enabled.
|
|
757
1049
|
// When layout === 'position' we only translate.
|
|
758
1050
|
// When layout === true we also scale to smoothly interpolate size changes.
|
|
759
|
-
let lastRect:
|
|
1051
|
+
let lastRect: RectLike | null = null
|
|
760
1052
|
$effect(() => {
|
|
761
1053
|
if (!(element && layoutProp && isLoaded === 'ready')) return
|
|
762
1054
|
|
|
763
|
-
// Initialize last rect on first ready frame
|
|
764
|
-
|
|
1055
|
+
// Initialize last rect on first ready frame. We measure through the
|
|
1056
|
+
// projection node rather than `measureRect` directly so the rect is
|
|
1057
|
+
// ancestor-transform-invariant: the node resets the whole ancestor
|
|
1058
|
+
// chain to its mount-time base while reading, which strips any
|
|
1059
|
+
// motion-applied (FLIP/drag) transform up the tree. Without this a
|
|
1060
|
+
// child re-measures and FLIPs whenever an ancestor's transform
|
|
1061
|
+
// animates, even though the child's own layout never moved. (#379)
|
|
1062
|
+
lastRect = measureLayoutRect()
|
|
765
1063
|
// Hint compositor for smoother FLIP transforms
|
|
766
1064
|
setCompositorHints(element!, true)
|
|
767
1065
|
|
|
768
1066
|
let rafId: number | null = null
|
|
769
1067
|
const runFlip = () => {
|
|
770
|
-
|
|
1068
|
+
// commitLayoutChange does the ancestor-stripped measure, fans
|
|
1069
|
+
// the delta out to `onProjectionUpdate` listeners, AND returns
|
|
1070
|
+
// the freshly-measured box — so the FLIP reuses that single
|
|
1071
|
+
// measurement as `next` instead of walking + transform-resetting
|
|
1072
|
+
// the ancestor chain a second time per frame. (#379)
|
|
1073
|
+
const nextBox = projection.commitLayoutChange()
|
|
1074
|
+
if (!nextBox) return
|
|
1075
|
+
const next = boxToRectLike(nextBox)
|
|
771
1076
|
if (!lastRect) {
|
|
772
|
-
lastRect =
|
|
1077
|
+
lastRect = next
|
|
773
1078
|
return
|
|
774
1079
|
}
|
|
775
|
-
const next = measureRect(element!, scrollContainers)
|
|
776
1080
|
const transforms = computeFlipTransforms(lastRect, next, layoutProp ?? false)
|
|
777
1081
|
runFlipAnimation(element!, transforms, (mergedTransition ?? {}) as AnimationOptions)
|
|
778
1082
|
lastRect = next
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { motion } from './motion';
|
|
|
6
6
|
export { animate, delay, hover, inView, press, resize, scroll, stagger, transform } from 'motion';
|
|
7
7
|
export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cubicBezier, easeIn, easeInOut, easeOut } from 'motion';
|
|
8
8
|
export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
|
|
9
|
-
export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, ReducedMotionConfig, Variants } from './types';
|
|
9
|
+
export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionOnProjectionUpdate, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, ProjectionUpdatePayload, ReducedMotionConfig, Variants } from './types';
|
|
10
10
|
export { useAnimate } from './utils/animate.svelte';
|
|
11
11
|
export type { AnimationScope } from './utils/animate.svelte';
|
|
12
12
|
export { useAnimationFrame } from './utils/animationFrame';
|
package/dist/types.d.ts
CHANGED
|
@@ -246,6 +246,68 @@ export type MotionOnDrag = ((event: PointerEvent, info: DragInfo) => void) | und
|
|
|
246
246
|
export type MotionOnDragEnd = ((event: PointerEvent, info: DragInfo) => void) | undefined;
|
|
247
247
|
export type MotionOnDirectionLock = ((axis: 'x' | 'y') => void) | undefined;
|
|
248
248
|
export type MotionOnDragTransitionEnd = (() => void) | undefined;
|
|
249
|
+
/**
|
|
250
|
+
* Pan-gesture callbacks. PanInfo is structurally identical to DragInfo
|
|
251
|
+
* (`{ point, delta, offset, velocity }`), so we re-use the type rather
|
|
252
|
+
* than ship a parallel alias.
|
|
253
|
+
*/
|
|
254
|
+
export type MotionOnPanSessionStart = ((event: PointerEvent, info: DragInfo) => void) | undefined;
|
|
255
|
+
export type MotionOnPanStart = ((event: PointerEvent, info: DragInfo) => void) | undefined;
|
|
256
|
+
export type MotionOnPan = ((event: PointerEvent, info: DragInfo) => void) | undefined;
|
|
257
|
+
export type MotionOnPanEnd = ((event: PointerEvent, info: DragInfo) => void) | undefined;
|
|
258
|
+
/**
|
|
259
|
+
* Payload delivered to `onProjectionUpdate`. Re-declared structurally
|
|
260
|
+
* here (rather than imported from `projection.ts`) so `types.ts`
|
|
261
|
+
* stays dependency-free at the type layer. `delta.x/y.translate` is the
|
|
262
|
+
* px shift the element's layout box moved between the pre-change
|
|
263
|
+
* snapshot and the post-change measurement; `hasLayoutChanged` is false
|
|
264
|
+
* only when the delta is within a tight float epsilon (±0.01px
|
|
265
|
+
* translate), so even a sub-pixel real move is reported as a change.
|
|
266
|
+
*/
|
|
267
|
+
export type ProjectionUpdatePayload = {
|
|
268
|
+
layout: {
|
|
269
|
+
x: {
|
|
270
|
+
min: number;
|
|
271
|
+
max: number;
|
|
272
|
+
};
|
|
273
|
+
y: {
|
|
274
|
+
min: number;
|
|
275
|
+
max: number;
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
snapshot: {
|
|
279
|
+
x: {
|
|
280
|
+
min: number;
|
|
281
|
+
max: number;
|
|
282
|
+
};
|
|
283
|
+
y: {
|
|
284
|
+
min: number;
|
|
285
|
+
max: number;
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
delta: {
|
|
289
|
+
x: {
|
|
290
|
+
translate: number;
|
|
291
|
+
scale: number;
|
|
292
|
+
origin: number;
|
|
293
|
+
originPoint: number;
|
|
294
|
+
};
|
|
295
|
+
y: {
|
|
296
|
+
translate: number;
|
|
297
|
+
scale: number;
|
|
298
|
+
origin: number;
|
|
299
|
+
originPoint: number;
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
hasLayoutChanged: boolean;
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* Fires after each layout change to a `motion.*` element that has
|
|
306
|
+
* `layout` enabled, with the FLIP delta between the pre- and
|
|
307
|
+
* post-change layout boxes. Mirrors framer-motion's `onLayoutMeasure`
|
|
308
|
+
* surface. Wired through the element's internal `ProjectionNode`.
|
|
309
|
+
*/
|
|
310
|
+
export type MotionOnProjectionUpdate = ((data: ProjectionUpdatePayload) => void) | undefined;
|
|
249
311
|
export type DragAxis = boolean | 'x' | 'y';
|
|
250
312
|
export type DragConstraints = {
|
|
251
313
|
top?: number;
|
|
@@ -319,6 +381,8 @@ export type MotionProps = {
|
|
|
319
381
|
whileFocus?: MotionWhileFocus;
|
|
320
382
|
/** Drag interaction animation */
|
|
321
383
|
whileDrag?: MotionWhileDrag;
|
|
384
|
+
/** Pan interaction animation — applied while a pan gesture is active */
|
|
385
|
+
whilePan?: MotionWhileDrag;
|
|
322
386
|
/** In-view interaction animation - animates when element enters viewport */
|
|
323
387
|
whileInView?: MotionWhileInView;
|
|
324
388
|
/** IntersectionObserver options for `whileInView` (once / root / margin / amount) */
|
|
@@ -355,12 +419,26 @@ export type MotionProps = {
|
|
|
355
419
|
onDirectionLock?: MotionOnDirectionLock;
|
|
356
420
|
/** Called when the post-drag transition finishes on all axes */
|
|
357
421
|
onDragTransitionEnd?: MotionOnDragTransitionEnd;
|
|
422
|
+
/** Pan gesture: fires once per pointerdown, before threshold */
|
|
423
|
+
onPanSessionStart?: MotionOnPanSessionStart;
|
|
424
|
+
/** Pan gesture: fires the first frame after offset crosses threshold */
|
|
425
|
+
onPanStart?: MotionOnPanStart;
|
|
426
|
+
/** Pan gesture: fires once per frame while panning */
|
|
427
|
+
onPan?: MotionOnPan;
|
|
428
|
+
/** Pan gesture: fires on pointerup if onPanStart ever fired */
|
|
429
|
+
onPanEnd?: MotionOnPanEnd;
|
|
358
430
|
/** Inline styles */
|
|
359
431
|
style?: string;
|
|
360
432
|
/** CSS classes */
|
|
361
433
|
class?: string;
|
|
362
434
|
/** Enable FLIP layout animations; "position" limits to translation only */
|
|
363
435
|
layout?: boolean | 'position';
|
|
436
|
+
/**
|
|
437
|
+
* Fires after each `layout`-driven change with the FLIP delta from
|
|
438
|
+
* the element's internal projection node. Mirrors framer-motion's
|
|
439
|
+
* `onLayoutMeasure`. Requires `layout` to be enabled.
|
|
440
|
+
*/
|
|
441
|
+
onProjectionUpdate?: MotionOnProjectionUpdate;
|
|
364
442
|
/** Shared layout animation identifier. Elements with matching layoutId animate between positions. */
|
|
365
443
|
layoutId?: string;
|
|
366
444
|
/**
|