@humanspeak/svelte-motion 0.4.4 → 0.4.5
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.
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deferred references to the chain of `layoutScroll` ancestors for the
|
|
3
|
+
* current subtree. Returned as a thunk because element refs are bound
|
|
4
|
+
* after mount; consumers invoke at measurement time.
|
|
5
|
+
*
|
|
6
|
+
* Order is closest-first. Order doesn't matter for the current scroll-
|
|
7
|
+
* offset sum, but is preserved so future per-container semantics (e.g.
|
|
8
|
+
* a `scroll.wasRoot` marker like framer-motion) can iterate deterministically.
|
|
9
|
+
*/
|
|
10
|
+
export type LayoutScrollContainerRef = () => Array<HTMLElement | null | undefined>;
|
|
11
|
+
/**
|
|
12
|
+
* Publish the scroll-container chain for descendant motion components.
|
|
13
|
+
*
|
|
14
|
+
* Called on a `motion.*` component with `layoutScroll` enabled during
|
|
15
|
+
* its init phase. The provided thunk should resolve to `[...ancestorChain,
|
|
16
|
+
* ownElement]` — descendants get the full chain in one call.
|
|
17
|
+
*
|
|
18
|
+
* Mirrors framer-motion's `removeElementScroll`, which walks the path
|
|
19
|
+
* and sums every `layoutScroll` ancestor's offset.
|
|
20
|
+
*
|
|
21
|
+
* @param ref Thunk returning the full ancestor chain (closest first).
|
|
22
|
+
*/
|
|
23
|
+
export declare const setLayoutScrollContainer: (ref: LayoutScrollContainerRef) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Capture the ancestor chain thunk at component init.
|
|
26
|
+
*
|
|
27
|
+
* Important: call this **before** the same component calls
|
|
28
|
+
* `setLayoutScrollContainer(...)`. Otherwise the lookup returns the
|
|
29
|
+
* component's own thunk (Svelte `setContext` shadows from the call site
|
|
30
|
+
* down) and the chain collapses.
|
|
31
|
+
*
|
|
32
|
+
* Returns `undefined` when no ancestor has `layoutScroll`.
|
|
33
|
+
*
|
|
34
|
+
* @returns Ancestor chain thunk, or `undefined`.
|
|
35
|
+
*/
|
|
36
|
+
export declare const getLayoutScrollContainerRef: () => LayoutScrollContainerRef | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const LAYOUT_SCROLL_CONTEXT_KEY = Symbol('layout-scroll-container');
|
|
3
|
+
/**
|
|
4
|
+
* Publish the scroll-container chain for descendant motion components.
|
|
5
|
+
*
|
|
6
|
+
* Called on a `motion.*` component with `layoutScroll` enabled during
|
|
7
|
+
* its init phase. The provided thunk should resolve to `[...ancestorChain,
|
|
8
|
+
* ownElement]` — descendants get the full chain in one call.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors framer-motion's `removeElementScroll`, which walks the path
|
|
11
|
+
* and sums every `layoutScroll` ancestor's offset.
|
|
12
|
+
*
|
|
13
|
+
* @param ref Thunk returning the full ancestor chain (closest first).
|
|
14
|
+
*/
|
|
15
|
+
export const setLayoutScrollContainer = (ref) => {
|
|
16
|
+
setContext(LAYOUT_SCROLL_CONTEXT_KEY, ref);
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Capture the ancestor chain thunk at component init.
|
|
20
|
+
*
|
|
21
|
+
* Important: call this **before** the same component calls
|
|
22
|
+
* `setLayoutScrollContainer(...)`. Otherwise the lookup returns the
|
|
23
|
+
* component's own thunk (Svelte `setContext` shadows from the call site
|
|
24
|
+
* down) and the chain collapses.
|
|
25
|
+
*
|
|
26
|
+
* Returns `undefined` when no ancestor has `layoutScroll`.
|
|
27
|
+
*
|
|
28
|
+
* @returns Ancestor chain thunk, or `undefined`.
|
|
29
|
+
*/
|
|
30
|
+
export const getLayoutScrollContainerRef = () => {
|
|
31
|
+
return getContext(LAYOUT_SCROLL_CONTEXT_KEY);
|
|
32
|
+
};
|
|
@@ -65,6 +65,10 @@
|
|
|
65
65
|
SVG_NAMESPACE
|
|
66
66
|
} from '../utils/svg'
|
|
67
67
|
import { getLayoutIdRegistry } from '../utils/layoutId'
|
|
68
|
+
import {
|
|
69
|
+
getLayoutScrollContainerRef,
|
|
70
|
+
setLayoutScrollContainer
|
|
71
|
+
} from '../components/layoutScroll.context'
|
|
68
72
|
|
|
69
73
|
type Props = MotionProps & {
|
|
70
74
|
children?: Snippet
|
|
@@ -118,6 +122,7 @@
|
|
|
118
122
|
dragControls: dragControlsProp,
|
|
119
123
|
layout: layoutProp,
|
|
120
124
|
layoutId: layoutIdProp,
|
|
125
|
+
layoutScroll: layoutScrollProp,
|
|
121
126
|
ref: element = $bindable(null),
|
|
122
127
|
...rest
|
|
123
128
|
}: Props = $props()
|
|
@@ -142,6 +147,30 @@
|
|
|
142
147
|
// Get layoutId registry (provided by AnimatePresence or a parent LayoutGroup)
|
|
143
148
|
const layoutIdRegistry = getLayoutIdRegistry()
|
|
144
149
|
|
|
150
|
+
// Capture the ancestor `layoutScroll` chain BEFORE we potentially shadow
|
|
151
|
+
// the context with ourselves below — this element's own FLIP measurements
|
|
152
|
+
// must resolve against the *ancestors*' scroll containers, not against
|
|
153
|
+
// itself.
|
|
154
|
+
//
|
|
155
|
+
// We walk the full chain (not just the nearest) so a `layoutScroll`
|
|
156
|
+
// outside another `layoutScroll` still contributes to descendant
|
|
157
|
+
// measurements — matches framer-motion's `removeElementScroll` walking
|
|
158
|
+
// `this.path`.
|
|
159
|
+
const ancestorScrollContainerRef = getLayoutScrollContainerRef()
|
|
160
|
+
if (layoutScrollProp) {
|
|
161
|
+
// Publish [...ancestorChain, ownElement]. The chain is collected
|
|
162
|
+
// lazily because element refs bind after mount.
|
|
163
|
+
setLayoutScrollContainer(() => {
|
|
164
|
+
const inherited = ancestorScrollContainerRef?.() ?? []
|
|
165
|
+
return element ? [...inherited, element] : inherited
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
const resolveLayoutScrollAncestors = (): HTMLElement[] => {
|
|
169
|
+
const refs = ancestorScrollContainerRef?.() ?? []
|
|
170
|
+
// Filter out unbound refs (HTMLElement | null | undefined → HTMLElement[]).
|
|
171
|
+
return refs.filter((el): el is HTMLElement => Boolean(el))
|
|
172
|
+
}
|
|
173
|
+
|
|
145
174
|
// Get current presence depth (0 = direct child of AnimatePresence, undefined = not in AnimatePresence)
|
|
146
175
|
const presenceDepth = getPresenceDepth()
|
|
147
176
|
|
|
@@ -213,11 +242,14 @@
|
|
|
213
242
|
$effect(() => {
|
|
214
243
|
if (!(element && layoutIdProp && layoutIdRegistry)) return
|
|
215
244
|
|
|
216
|
-
// Capture rect on every frame while mounted
|
|
245
|
+
// Capture rect on every frame while mounted. Re-express in the
|
|
246
|
+
// nearest layoutScroll ancestor's coordinate space so the FLIP-from
|
|
247
|
+
// rect stored at unmount stays correct even if the scroll container
|
|
248
|
+
// moved between the snapshot and the next element's mount.
|
|
217
249
|
let rafId: number
|
|
218
250
|
const captureRect = () => {
|
|
219
251
|
if (element) {
|
|
220
|
-
layoutIdLastRect = element
|
|
252
|
+
layoutIdLastRect = measureRect(element, resolveLayoutScrollAncestors())
|
|
221
253
|
}
|
|
222
254
|
rafId = requestAnimationFrame(captureRect)
|
|
223
255
|
}
|
|
@@ -701,17 +733,18 @@
|
|
|
701
733
|
if (!(element && layoutProp && isLoaded === 'ready')) return
|
|
702
734
|
|
|
703
735
|
// Initialize last rect on first ready frame
|
|
704
|
-
lastRect = measureRect(element
|
|
736
|
+
lastRect = measureRect(element!, resolveLayoutScrollAncestors())
|
|
705
737
|
// Hint compositor for smoother FLIP transforms
|
|
706
738
|
setCompositorHints(element!, true)
|
|
707
739
|
|
|
708
740
|
let rafId: number | null = null
|
|
709
741
|
const runFlip = () => {
|
|
742
|
+
const scrollContainers = resolveLayoutScrollAncestors()
|
|
710
743
|
if (!lastRect) {
|
|
711
|
-
lastRect = measureRect(element
|
|
744
|
+
lastRect = measureRect(element!, scrollContainers)
|
|
712
745
|
return
|
|
713
746
|
}
|
|
714
|
-
const next = measureRect(element
|
|
747
|
+
const next = measureRect(element!, scrollContainers)
|
|
715
748
|
const transforms = computeFlipTransforms(lastRect, next, layoutProp ?? false)
|
|
716
749
|
runFlipAnimation(element!, transforms, (mergedTransition ?? {}) as AnimationOptions)
|
|
717
750
|
lastRect = next
|
|
@@ -745,7 +778,7 @@
|
|
|
745
778
|
const prev = layoutIdRegistry.consume(layoutIdProp)
|
|
746
779
|
if (!prev) return // First appearance, no animation needed
|
|
747
780
|
|
|
748
|
-
const next = measureRect(element)
|
|
781
|
+
const next = measureRect(element, resolveLayoutScrollAncestors())
|
|
749
782
|
const transforms = computeFlipTransforms(prev.rect, next, true)
|
|
750
783
|
|
|
751
784
|
setCompositorHints(element, true)
|
package/dist/types.d.ts
CHANGED
|
@@ -335,6 +335,22 @@ export type MotionProps = {
|
|
|
335
335
|
layout?: boolean | 'position';
|
|
336
336
|
/** Shared layout animation identifier. Elements with matching layoutId animate between positions. */
|
|
337
337
|
layoutId?: string;
|
|
338
|
+
/**
|
|
339
|
+
* Mark this element as a scroll container so descendant `layout` animations
|
|
340
|
+
* measure rects in this container's coordinate space. Without it, scrolling
|
|
341
|
+
* mid-animation makes the FLIP transform fight the scroll and the layout
|
|
342
|
+
* animation drifts.
|
|
343
|
+
*
|
|
344
|
+
* Apply on the same element as `overflow: scroll` / `overflow: auto`.
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```svelte
|
|
348
|
+
* <motion.div layoutScroll style="overflow: auto">
|
|
349
|
+
* <motion.div layout />
|
|
350
|
+
* </motion.div>
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
layoutScroll?: boolean;
|
|
338
354
|
/** Ref to the element */
|
|
339
355
|
ref?: HTMLElement | null;
|
|
340
356
|
/** Enable drag gestures. true for both axes, or lock to 'x'/'y'. */
|
package/dist/utils/layout.d.ts
CHANGED
|
@@ -5,10 +5,32 @@ import { type AnimationOptions } from 'motion';
|
|
|
5
5
|
* Temporarily clears `transform` to avoid skewing measurements, restoring it
|
|
6
6
|
* immediately after reading the rect.
|
|
7
7
|
*
|
|
8
|
+
* When `scrollContainers` are provided, the returned rect is shifted by the
|
|
9
|
+
* **sum** of each container's `scrollLeft` / `scrollTop`. FLIP deltas
|
|
10
|
+
* computed from two such measures stay correct even when the user scrolls
|
|
11
|
+
* any of the containers between measurements — including a nested
|
|
12
|
+
* `layoutScroll` inside another `layoutScroll`. Mirrors framer-motion's
|
|
13
|
+
* `removeElementScroll`, which walks every ancestor in the path.
|
|
14
|
+
*
|
|
15
|
+
* Pass an empty array (or omit) for viewport-relative behaviour.
|
|
16
|
+
*
|
|
8
17
|
* @param el Element to measure.
|
|
9
|
-
* @
|
|
18
|
+
* @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
|
|
19
|
+
* @returns DOMRect snapshot of the element.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* // No scroll containers — viewport-relative rect.
|
|
24
|
+
* const rect = measureRect(node)
|
|
25
|
+
*
|
|
26
|
+
* // Single ancestor scroll container (one `layoutScroll`).
|
|
27
|
+
* const rect = measureRect(node, [scrollPanel])
|
|
28
|
+
*
|
|
29
|
+
* // Nested `layoutScroll` ancestors — sums offsets from every container.
|
|
30
|
+
* const rect = measureRect(node, [innerScroll, outerScroll])
|
|
31
|
+
* ```
|
|
10
32
|
*/
|
|
11
|
-
export declare const measureRect: (el: HTMLElement) => DOMRect;
|
|
33
|
+
export declare const measureRect: (el: HTMLElement, scrollContainers?: HTMLElement[]) => DOMRect;
|
|
12
34
|
/**
|
|
13
35
|
* Compute FLIP transform deltas between two rects.
|
|
14
36
|
*
|
package/dist/utils/layout.js
CHANGED
|
@@ -5,14 +5,49 @@ import { animate } from 'motion';
|
|
|
5
5
|
* Temporarily clears `transform` to avoid skewing measurements, restoring it
|
|
6
6
|
* immediately after reading the rect.
|
|
7
7
|
*
|
|
8
|
+
* When `scrollContainers` are provided, the returned rect is shifted by the
|
|
9
|
+
* **sum** of each container's `scrollLeft` / `scrollTop`. FLIP deltas
|
|
10
|
+
* computed from two such measures stay correct even when the user scrolls
|
|
11
|
+
* any of the containers between measurements — including a nested
|
|
12
|
+
* `layoutScroll` inside another `layoutScroll`. Mirrors framer-motion's
|
|
13
|
+
* `removeElementScroll`, which walks every ancestor in the path.
|
|
14
|
+
*
|
|
15
|
+
* Pass an empty array (or omit) for viewport-relative behaviour.
|
|
16
|
+
*
|
|
8
17
|
* @param el Element to measure.
|
|
9
|
-
* @
|
|
18
|
+
* @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
|
|
19
|
+
* @returns DOMRect snapshot of the element.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* // No scroll containers — viewport-relative rect.
|
|
24
|
+
* const rect = measureRect(node)
|
|
25
|
+
*
|
|
26
|
+
* // Single ancestor scroll container (one `layoutScroll`).
|
|
27
|
+
* const rect = measureRect(node, [scrollPanel])
|
|
28
|
+
*
|
|
29
|
+
* // Nested `layoutScroll` ancestors — sums offsets from every container.
|
|
30
|
+
* const rect = measureRect(node, [innerScroll, outerScroll])
|
|
31
|
+
* ```
|
|
10
32
|
*/
|
|
11
|
-
export const measureRect = (el) => {
|
|
33
|
+
export const measureRect = (el, scrollContainers) => {
|
|
12
34
|
const prev = el.style.transform;
|
|
13
35
|
try {
|
|
14
36
|
el.style.transform = 'none';
|
|
15
|
-
|
|
37
|
+
const rect = el.getBoundingClientRect();
|
|
38
|
+
if (!scrollContainers || scrollContainers.length === 0)
|
|
39
|
+
return rect;
|
|
40
|
+
// Re-express the rect in the *combined* scroll-container coordinate
|
|
41
|
+
// space so a subsequent scroll on any of them doesn't show up as
|
|
42
|
+
// movement. DOMRect's left/top are read-only, so allocate a fresh
|
|
43
|
+
// one with the summed offsets applied.
|
|
44
|
+
let offsetLeft = 0;
|
|
45
|
+
let offsetTop = 0;
|
|
46
|
+
for (const container of scrollContainers) {
|
|
47
|
+
offsetLeft += container.scrollLeft;
|
|
48
|
+
offsetTop += container.scrollTop;
|
|
49
|
+
}
|
|
50
|
+
return new DOMRect(rect.left + offsetLeft, rect.top + offsetTop, rect.width, rect.height);
|
|
16
51
|
}
|
|
17
52
|
finally {
|
|
18
53
|
el.style.transform = prev;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Framer Motion for Svelte 5. Declarative motion.<tag> components with AnimatePresence exit animations, gestures (hover, tap, drag, focus, in-view), variants, FLIP layout animations, shared-layout transitions, spring physics, and scroll-linked motion values. The drop-in Framer Motion alternative for Svelte and SvelteKit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|