solidjs-motion 0.1.0
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 +117 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2627 -0
- package/dist/index.js.map +1 -0
- package/dist/src/default-values.d.ts +6 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/motion-config.d.ts +14 -0
- package/dist/src/motion-proxy.d.ts +103 -0
- package/dist/src/presence-context.d.ts +4 -0
- package/dist/src/presence.d.ts +95 -0
- package/dist/src/primitives/createDrag.d.ts +16 -0
- package/dist/src/primitives/createDragControls.d.ts +30 -0
- package/dist/src/primitives/createGestures.d.ts +8 -0
- package/dist/src/primitives/createInView.d.ts +51 -0
- package/dist/src/primitives/createMotion.d.ts +82 -0
- package/dist/src/primitives/createPan.d.ts +83 -0
- package/dist/src/primitives/createScroll.d.ts +40 -0
- package/dist/src/primitives/gesture-state.d.ts +108 -0
- package/dist/src/primitives/motion-value.d.ts +111 -0
- package/dist/src/primitives/value-registry.d.ts +34 -0
- package/dist/src/reduced-motion.d.ts +29 -0
- package/dist/src/style.d.ts +79 -0
- package/dist/src/types.d.ts +374 -0
- package/dist/src/use-motion.d.ts +35 -0
- package/dist/src/variants.d.ts +64 -0
- package/package.json +78 -0
- package/src/default-values.ts +52 -0
- package/src/index.ts +60 -0
- package/src/motion-config.tsx +37 -0
- package/src/motion-proxy.tsx +377 -0
- package/src/presence-context.ts +32 -0
- package/src/presence.tsx +466 -0
- package/src/primitives/createDrag.ts +670 -0
- package/src/primitives/createDragControls.ts +101 -0
- package/src/primitives/createGestures.ts +145 -0
- package/src/primitives/createInView.ts +124 -0
- package/src/primitives/createMotion.ts +638 -0
- package/src/primitives/createPan.ts +338 -0
- package/src/primitives/createScroll.ts +101 -0
- package/src/primitives/gesture-state.ts +772 -0
- package/src/primitives/motion-value.ts +328 -0
- package/src/primitives/value-registry.ts +114 -0
- package/src/reduced-motion.ts +51 -0
- package/src/style.ts +266 -0
- package/src/types.ts +538 -0
- package/src/use-motion.tsx +412 -0
- package/src/variants.ts +134 -0
package/src/presence.tsx
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { resolveElements } from "@solid-primitives/refs"
|
|
2
|
+
import { createListTransition, createSwitchTransition } from "@solid-primitives/transition-group"
|
|
3
|
+
import {
|
|
4
|
+
type Component,
|
|
5
|
+
createEffect,
|
|
6
|
+
createMemo,
|
|
7
|
+
createSignal,
|
|
8
|
+
type JSX,
|
|
9
|
+
Match,
|
|
10
|
+
onCleanup,
|
|
11
|
+
Switch,
|
|
12
|
+
} from "solid-js"
|
|
13
|
+
import { PresenceContext } from "./presence-context"
|
|
14
|
+
import type { MotionElement, PresenceContextValue } from "./types"
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// <Presence> — exit-animation coordinator for motion children.
|
|
18
|
+
//
|
|
19
|
+
// Architecture (see ADR 0003):
|
|
20
|
+
// - Wraps a conditional JSX subtree (typically `<Show>`, `<For>`, or
|
|
21
|
+
// `<Index>` containing `<motion.*>` elements with `exit` declared).
|
|
22
|
+
// - Resolves children via @solid-primitives/refs (`resolveFirst` /
|
|
23
|
+
// `resolveElements`), then routes them through
|
|
24
|
+
// @solid-primitives/transition-group's `createSwitchTransition` (single)
|
|
25
|
+
// or `createListTransition` (list) which keeps exiting elements in the
|
|
26
|
+
// DOM until we explicitly call `done()`.
|
|
27
|
+
// - Each motion child registers a `runExit` callable via PresenceContext
|
|
28
|
+
// when its `createMotion` runs. The callable flips the state machine's
|
|
29
|
+
// `exit` slot and resolves when the resulting animate settles.
|
|
30
|
+
// - On removal, transition-group calls our `onExit(el, done)`. We look up
|
|
31
|
+
// the registered `runExit` and chain `done` onto its promise. If no
|
|
32
|
+
// `runExit` is registered (a non-motion child, or a motion child with no
|
|
33
|
+
// `exit` prop), `done()` fires immediately and the element disappears.
|
|
34
|
+
//
|
|
35
|
+
// SSR: same JSX shape as client (Provider + PresenceCore + Switch/Match).
|
|
36
|
+
// transition-group's helpers are SSR-safe — they pass children through when
|
|
37
|
+
// no DOM refs have fired — so structural divergence (which would break
|
|
38
|
+
// Solid's hydration marker alignment) is avoided. Each child's initial
|
|
39
|
+
// style is still emitted via the existing `useMotion` SSR contract.
|
|
40
|
+
//
|
|
41
|
+
// Single-vs-list dispatch is decided at first resolution and stable for the
|
|
42
|
+
// Presence instance's lifetime — switching mid-life would require torn-down
|
|
43
|
+
// transition-group state, which neither helper supports. Document the
|
|
44
|
+
// constraint (rare in practice; conditional rendering rarely flips between
|
|
45
|
+
// "one item" and "many items" without unmount).
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Internal helper: maps the public `mode` prop to the value
|
|
50
|
+
* `createSwitchTransition` expects. `popLayout` is intentionally deferred
|
|
51
|
+
* (layout animations are v0.2+); `wait` maps to `out-in`; `sync` is the
|
|
52
|
+
* default and corresponds to transition-group's `parallel`.
|
|
53
|
+
*/
|
|
54
|
+
function switchMode(mode: PresenceProps["mode"]): "out-in" | "parallel" {
|
|
55
|
+
return mode === "wait" ? "out-in" : "parallel"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Find every motion child registered under `root` (or root itself) and
|
|
60
|
+
* return their [element, runExit] pairs. Walks the `runExits` map and
|
|
61
|
+
* tests containment via `Node.contains`, which is O(depth) per check —
|
|
62
|
+
* cheap because n is bounded by the number of motion children Presence
|
|
63
|
+
* is tracking. Order isn't load-bearing; callers Promise.all the runExits.
|
|
64
|
+
*/
|
|
65
|
+
function collectSubtreeExits(
|
|
66
|
+
root: MotionElement,
|
|
67
|
+
runExits: Map<MotionElement, () => Promise<void>>,
|
|
68
|
+
): Array<[MotionElement, () => Promise<void>]> {
|
|
69
|
+
const out: Array<[MotionElement, () => Promise<void>]> = []
|
|
70
|
+
for (const [el, fn] of runExits) {
|
|
71
|
+
if (el === root || root.contains(el)) out.push([el, fn])
|
|
72
|
+
}
|
|
73
|
+
return out
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type PresenceProps = {
|
|
77
|
+
/**
|
|
78
|
+
* Exit/enter coordination.
|
|
79
|
+
* - `"sync"` (default) — exit and enter overlap (transition-group's
|
|
80
|
+
* `parallel`). Best for list animations and independent items.
|
|
81
|
+
* - `"wait"` — old child fully exits before the new one enters
|
|
82
|
+
* (transition-group's `out-in`). Single-child only; ignored (with a
|
|
83
|
+
* dev-mode warning) when wrapping a list.
|
|
84
|
+
*/
|
|
85
|
+
mode?: "sync" | "wait"
|
|
86
|
+
/**
|
|
87
|
+
* Animate children on first mount. Defaults to `true` (matches motion-
|
|
88
|
+
* react). Set `false` to suppress the entry animation for the very first
|
|
89
|
+
* child(ren); subsequent mounts mid-life still animate.
|
|
90
|
+
*/
|
|
91
|
+
initial?: boolean
|
|
92
|
+
/**
|
|
93
|
+
* List-path only — controls what transition-group does with an exiting
|
|
94
|
+
* element while its `exit` animation is playing. Forwarded directly to
|
|
95
|
+
* `@solid-primitives/transition-group`'s `createListTransition`.
|
|
96
|
+
*
|
|
97
|
+
* - `"move-to-end"` (default) — the exiting element is appended to the
|
|
98
|
+
* end of the rendered array so its DOM position changes during exit.
|
|
99
|
+
* Fine for grids/cascades; surprising for vertically-stacked toasts
|
|
100
|
+
* because surviving siblings JUMP up while the dismissed item is
|
|
101
|
+
* still fading out below them.
|
|
102
|
+
* - `"keep-index"` — the exiting element stays at its original index
|
|
103
|
+
* until exit completes. Surviving siblings don't reflow until the
|
|
104
|
+
* slot is released. Best default for notification stacks.
|
|
105
|
+
* - `"remove"` — no exit transition; the element is gone from the
|
|
106
|
+
* rendered array immediately. Useful when the exit is purely visual
|
|
107
|
+
* on the child (e.g., it self-animates via opacity transitions
|
|
108
|
+
* instead of `exit`).
|
|
109
|
+
*/
|
|
110
|
+
exitMethod?: "move-to-end" | "keep-index" | "remove"
|
|
111
|
+
children: JSX.Element
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Wraps a conditional or iterated JSX subtree and runs the descendants'
|
|
116
|
+
* `exit` targets before they unmount. Matches motion-react's
|
|
117
|
+
* `<AnimatePresence>` shape but with Solid's `<Show>` / `<For>` /
|
|
118
|
+
* `<Index>` patterns instead of conditional children.
|
|
119
|
+
*
|
|
120
|
+
* Nested motion children are first-class: when an ancestor unmounts,
|
|
121
|
+
* Presence walks the subtree from each resolved child and fires every
|
|
122
|
+
* registered `runExit` it finds in parallel — including motion children
|
|
123
|
+
* nested inside plain wrappers, or descendants whose `exit` label was
|
|
124
|
+
* cascaded down via `m.Provider`. Each motion descendant animates with
|
|
125
|
+
* its own variant/target; transition-group only releases the DOM once
|
|
126
|
+
* the combined `Promise.all` settles. Mirrors motion-react's behavior
|
|
127
|
+
* where a `<motion.div exit={...}>` inside an `<AnimatePresence>` boundary
|
|
128
|
+
* animates correctly regardless of depth.
|
|
129
|
+
*
|
|
130
|
+
* @example Single (conditional unmount)
|
|
131
|
+
* <Presence>
|
|
132
|
+
* <Show when={open()}>
|
|
133
|
+
* <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
|
134
|
+
* saved
|
|
135
|
+
* </motion.div>
|
|
136
|
+
* </Show>
|
|
137
|
+
* </Presence>
|
|
138
|
+
*
|
|
139
|
+
* @example List (items entering and exiting independently)
|
|
140
|
+
* <Presence>
|
|
141
|
+
* <For each={items()}>
|
|
142
|
+
* {(item) => (
|
|
143
|
+
* <motion.li exit={{ opacity: 0, x: 20 }}>{item.text}</motion.li>
|
|
144
|
+
* )}
|
|
145
|
+
* </For>
|
|
146
|
+
* </Presence>
|
|
147
|
+
*/
|
|
148
|
+
export const Presence: Component<PresenceProps> = (props) => {
|
|
149
|
+
// SSR: render the SAME JSX shape as the client (Provider + PresenceCore +
|
|
150
|
+
// <Switch>+<Match>) so Solid's hydration markers align. The transition-
|
|
151
|
+
// group helpers ARE SSR-safe — they return the source elements unchanged
|
|
152
|
+
// when no DOM refs have fired (i.e. during renderToString) — so calling
|
|
153
|
+
// them server-side is harmless and avoids the structural divergence that
|
|
154
|
+
// would otherwise produce hydration mismatches.
|
|
155
|
+
//
|
|
156
|
+
// The only server-time wrinkle is `<Presence initial={false}>`:
|
|
157
|
+
// useMotion's first-paint composition reads `PresenceContext.initial`
|
|
158
|
+
// and, when false, emits the ANIMATE target style instead of the
|
|
159
|
+
// initial. We honor that by setting the signal's seed value below.
|
|
160
|
+
//
|
|
161
|
+
// ---------- PresenceContext value supplied to descendants ----------
|
|
162
|
+
const runExits = new Map<MotionElement, () => Promise<void>>()
|
|
163
|
+
// Enter callbacks — symmetric to runExits. createMotion registers a
|
|
164
|
+
// `runEnter` when it's inside this Presence; we fire it the moment the
|
|
165
|
+
// element is actually inserted into the DOM (from transition-group's
|
|
166
|
+
// onEnter / onChange.added). One-shot — deleted after firing so we don't
|
|
167
|
+
// re-trigger the enter animate on subsequent renders.
|
|
168
|
+
const runEnters = new Map<MotionElement, () => void>()
|
|
169
|
+
// `initial` is read at construction. We flip it to `true` after the first
|
|
170
|
+
// microtask so mid-life inserts DO animate even if the user set
|
|
171
|
+
// `initial={false}`. Matches motion-react's behavior.
|
|
172
|
+
const [presenceInitial, setPresenceInitial] = createSignal(props.initial ?? true)
|
|
173
|
+
queueMicrotask(() => setPresenceInitial(true))
|
|
174
|
+
|
|
175
|
+
const ctx: PresenceContextValue = {
|
|
176
|
+
register: (el, runExit) => {
|
|
177
|
+
runExits.set(el, runExit)
|
|
178
|
+
},
|
|
179
|
+
unregister: (el) => {
|
|
180
|
+
runExits.delete(el)
|
|
181
|
+
},
|
|
182
|
+
beforeUnmount: (el) => {
|
|
183
|
+
// Walk the subtree rooted at `el` and fire every registered runExit
|
|
184
|
+
// we find — the root's own (if registered) plus every descendant
|
|
185
|
+
// that's a motion child. They all run concurrently; we await the
|
|
186
|
+
// combined Promise.all so transition-group only releases the DOM
|
|
187
|
+
// once every motion descendant has finished its exit, then prune
|
|
188
|
+
// the entries from the registry.
|
|
189
|
+
//
|
|
190
|
+
// This is the mechanism that makes motion-react's parent-cascade
|
|
191
|
+
// exit pattern work for us: when an ancestor unmounts, each nested
|
|
192
|
+
// motion child still runs its OWN exit (its runExit closure already
|
|
193
|
+
// captures the element, `getOpts`, and the state-machine handles,
|
|
194
|
+
// so it doesn't matter that Solid has disposed the surrounding
|
|
195
|
+
// owner). Callers don't need to unregister individually — this
|
|
196
|
+
// method finishes the bookkeeping for the whole subtree.
|
|
197
|
+
const exiting = collectSubtreeExits(el, runExits)
|
|
198
|
+
if (exiting.length === 0) return Promise.resolve()
|
|
199
|
+
return Promise.all(exiting.map((pair) => pair[1]())).then(() => {
|
|
200
|
+
for (const [exitedEl] of exiting) runExits.delete(exitedEl)
|
|
201
|
+
})
|
|
202
|
+
},
|
|
203
|
+
registerEnter: (el, runEnter) => {
|
|
204
|
+
runEnters.set(el, runEnter)
|
|
205
|
+
},
|
|
206
|
+
beforeMount: (el) => {
|
|
207
|
+
const fn = runEnters.get(el)
|
|
208
|
+
runEnters.delete(el)
|
|
209
|
+
fn?.()
|
|
210
|
+
},
|
|
211
|
+
initial: presenceInitial,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// CRITICAL — the children-resolution dance lives INSIDE the Provider's
|
|
215
|
+
// JSX scope so that:
|
|
216
|
+
// (a) motion descendants' refs fire under the Provider's owner and
|
|
217
|
+
// `usePresenceContext` resolves to our `ctx` (not the no-op).
|
|
218
|
+
// (b) resolveElements is called from a stable, single-execution scope.
|
|
219
|
+
// solid-motionone's Presence (1.0.4) achieves this by inlining the
|
|
220
|
+
// transition-group call as the Provider's only child; we extend
|
|
221
|
+
// that to two paths by routing through a tiny `PresenceCore`
|
|
222
|
+
// subcomponent so the resolveElements memo isn't recreated.
|
|
223
|
+
return (
|
|
224
|
+
<PresenceContext.Provider value={ctx}>
|
|
225
|
+
<PresenceCore
|
|
226
|
+
source={() => props.children}
|
|
227
|
+
mode={props.mode}
|
|
228
|
+
exitMethod={props.exitMethod}
|
|
229
|
+
appear={presenceInitial}
|
|
230
|
+
ctx={ctx}
|
|
231
|
+
/>
|
|
232
|
+
</PresenceContext.Provider>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Internal subcomponent that owns the transition-group machinery. Running
|
|
238
|
+
// the resolution + transition-group setup inside a Solid component (rather
|
|
239
|
+
// than inline JSX or an IIFE) gives us a clean single-execution scope: the
|
|
240
|
+
// component's body runs ONCE per <Presence> instance, the resolveElements
|
|
241
|
+
// memo is set up ONCE, and the transition's createComputed is owned by this
|
|
242
|
+
// component's lifetime. The Provider's value still flows down because this
|
|
243
|
+
// component is rendered AS A CHILD of the Provider.
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
type PresenceCoreProps = {
|
|
246
|
+
source: () => JSX.Element
|
|
247
|
+
mode: PresenceProps["mode"]
|
|
248
|
+
exitMethod: PresenceProps["exitMethod"]
|
|
249
|
+
appear: () => boolean
|
|
250
|
+
ctx: PresenceContextValue
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const PresenceCore: Component<PresenceCoreProps> = (p) => {
|
|
254
|
+
// One resolveElements call — both paths read from it. Calling it twice
|
|
255
|
+
// (e.g. resolveFirst + resolveElements) would create two children-
|
|
256
|
+
// resolution memos and mount the motion descendants twice.
|
|
257
|
+
const resolved = resolveElements(p.source)
|
|
258
|
+
|
|
259
|
+
// Sticky single-vs-list decision — DEFERRED until the source resolves
|
|
260
|
+
// to actual data. The earlier "decide at construction" approach silently
|
|
261
|
+
// broke any `<For>` that started empty: `Array.isArray(null)` is false,
|
|
262
|
+
// so we'd lock into switch mode and drop every list item past the first.
|
|
263
|
+
// Using a memo with a self-prev short-circuit makes the decision sticky
|
|
264
|
+
// once a non-null value arrives, while still reacting to the first
|
|
265
|
+
// populated read whenever that happens.
|
|
266
|
+
const path = createMemo<"switch" | "list" | null>((prev) => {
|
|
267
|
+
if (prev) return prev
|
|
268
|
+
const v = resolved()
|
|
269
|
+
if (v == null) return null
|
|
270
|
+
return Array.isArray(v) ? "list" : "switch"
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
if (process.env.NODE_ENV !== "production") {
|
|
274
|
+
createEffect(() => {
|
|
275
|
+
if (path() === "list" && p.mode === "wait") {
|
|
276
|
+
console.warn(
|
|
277
|
+
'[solidjs-motion] <Presence mode="wait"> has no meaningful effect with a list of children — "wait" sequences a single exiting element before a single entering one. Use it with `<Show>`-style conditional rendering.',
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// transition-group's helpers return Accessor<Element[]>. Solid renders
|
|
284
|
+
// accessor functions inline (calls them in a tracking scope), but TS
|
|
285
|
+
// sees Accessor, not JSX.Element. The cast is a no-op at runtime.
|
|
286
|
+
//
|
|
287
|
+
// <Switch> + keyed <Match> renders nothing until the path is decided
|
|
288
|
+
// (the children render as null when both Match `when`s are false). The
|
|
289
|
+
// child function form on Match guarantees the create*Transition call
|
|
290
|
+
// happens AT MOST ONCE, on the branch that wins — both paths can't be
|
|
291
|
+
// set up against the same resolveElements memo.
|
|
292
|
+
return (
|
|
293
|
+
<Switch>
|
|
294
|
+
<Match when={path() === "switch"} keyed>
|
|
295
|
+
{(_v) =>
|
|
296
|
+
createSwitchTransition(
|
|
297
|
+
() => {
|
|
298
|
+
const v = resolved()
|
|
299
|
+
return Array.isArray(v) ? (v[0] ?? null) : v
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
appear: p.appear(),
|
|
303
|
+
mode: switchMode(p.mode),
|
|
304
|
+
onExit(el, done) {
|
|
305
|
+
// Disable pointer events on the exiting element. In sync
|
|
306
|
+
// mode transition-group keeps the old node in the DOM as
|
|
307
|
+
// a sibling of the new one (with the old one LATER in
|
|
308
|
+
// source order, putting it on top in z-stacking). Without
|
|
309
|
+
// this, the exiting node — even at opacity:0 mid-exit —
|
|
310
|
+
// intercepts pointer events intended for the incoming
|
|
311
|
+
// card, breaking drag and hover on the new element until
|
|
312
|
+
// the exit settles.
|
|
313
|
+
const motionEl = el as MotionElement
|
|
314
|
+
if (motionEl instanceof HTMLElement || motionEl instanceof SVGElement) {
|
|
315
|
+
;(motionEl.style as CSSStyleDeclaration).pointerEvents = "none"
|
|
316
|
+
}
|
|
317
|
+
// beforeUnmount walks the subtree, fires every descendant
|
|
318
|
+
// motion child's runExit in parallel, awaits the combined
|
|
319
|
+
// Promise.all, AND prunes the registry. We just chain
|
|
320
|
+
// done() onto it.
|
|
321
|
+
p.ctx.beforeUnmount(motionEl).then(done)
|
|
322
|
+
},
|
|
323
|
+
onEnter(el, done) {
|
|
324
|
+
// The element was just inserted into the DOM via
|
|
325
|
+
// setReturned (transition-group's createSwitchTransition
|
|
326
|
+
// does this synchronously before invoking onEnter). Fire
|
|
327
|
+
// the child's registered runEnter callback so its state
|
|
328
|
+
// machine can dispatch the first animate against a
|
|
329
|
+
// connected element — without this step, motion's
|
|
330
|
+
// `animate()` would have already completed off-DOM during
|
|
331
|
+
// the surrounding exit (or before the appear-driven
|
|
332
|
+
// insertion). Then unblock transition-group.
|
|
333
|
+
p.ctx.beforeMount?.(el as MotionElement)
|
|
334
|
+
done()
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
) as unknown as JSX.Element
|
|
338
|
+
}
|
|
339
|
+
</Match>
|
|
340
|
+
<Match when={path() === "list"} keyed>
|
|
341
|
+
{(_v) =>
|
|
342
|
+
createListTransition(() => resolved.toArray(), {
|
|
343
|
+
appear: p.appear(),
|
|
344
|
+
exitMethod: p.exitMethod,
|
|
345
|
+
onChange({ added, removed, finishRemoved }) {
|
|
346
|
+
// Fire enter callbacks for added elements first —
|
|
347
|
+
// createListTransition has already updated the source array
|
|
348
|
+
// (and Solid's render diff has inserted the new nodes)
|
|
349
|
+
// before `onChange` runs, so this is the analogue of the
|
|
350
|
+
// switch path's onEnter timing. Without it the new motion
|
|
351
|
+
// children's first animate would have dispatched off-DOM at
|
|
352
|
+
// template instantiation and lost their commitStyles.
|
|
353
|
+
for (const el of added) {
|
|
354
|
+
p.ctx.beforeMount?.(el as MotionElement)
|
|
355
|
+
}
|
|
356
|
+
if (removed.length === 0) return
|
|
357
|
+
// Disable pointer events on every exiting node (see
|
|
358
|
+
// switch-path onExit for the rationale — same z-stacking
|
|
359
|
+
// trap applies when a removed list item lingers as a
|
|
360
|
+
// sibling of new/unchanged ones).
|
|
361
|
+
for (const el of removed as MotionElement[]) {
|
|
362
|
+
if (el instanceof HTMLElement || el instanceof SVGElement) {
|
|
363
|
+
;(el.style as CSSStyleDeclaration).pointerEvents = "none"
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// beforeUnmount handles the subtree walk + unregister
|
|
367
|
+
// bookkeeping per root.
|
|
368
|
+
Promise.all((removed as MotionElement[]).map((el) => p.ctx.beforeUnmount(el))).then(
|
|
369
|
+
() => finishRemoved(removed),
|
|
370
|
+
)
|
|
371
|
+
},
|
|
372
|
+
}) as unknown as JSX.Element
|
|
373
|
+
}
|
|
374
|
+
</Match>
|
|
375
|
+
</Switch>
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
// useAnimatePresence — imperative hook variant for library authors.
|
|
381
|
+
//
|
|
382
|
+
// Returns `{ Provider, exit }`. The user wraps their own conditional
|
|
383
|
+
// rendering in the returned Provider and calls `exit()` to trigger exit
|
|
384
|
+
// animations on every motion child currently registered. Resolves when all
|
|
385
|
+
// settle.
|
|
386
|
+
//
|
|
387
|
+
// Use this when:
|
|
388
|
+
// - You're a library author whose internal mount state can't be a Solid
|
|
389
|
+
// `<Show>` (e.g., route transitions controlled by an external state
|
|
390
|
+
// machine; toast queues with non-Solid lifecycle).
|
|
391
|
+
// - You need imperative control over WHEN exits trigger (e.g., await
|
|
392
|
+
// network completion before unmounting).
|
|
393
|
+
//
|
|
394
|
+
// For 95% of application code, prefer `<Presence>` — it handles the
|
|
395
|
+
// children-resolver dance and list semantics automatically.
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
export type UseAnimatePresenceOptions = {
|
|
399
|
+
/** Same semantics as `<Presence initial>`. Defaults to `true`. */
|
|
400
|
+
initial?: boolean
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export type UseAnimatePresenceResult = {
|
|
404
|
+
/**
|
|
405
|
+
* Provider component to wrap your conditional rendering. Every motion
|
|
406
|
+
* descendant inside this Provider registers with the hook's internal
|
|
407
|
+
* registry and is reachable via `exit()`.
|
|
408
|
+
*/
|
|
409
|
+
Provider: Component<{ children: JSX.Element }>
|
|
410
|
+
/**
|
|
411
|
+
* Trigger exit on every motion child currently registered with this
|
|
412
|
+
* hook. Resolves when all exit animations have settled. Calling `exit()`
|
|
413
|
+
* does NOT unmount anything — the caller is responsible for flipping
|
|
414
|
+
* their mount signal once the promise resolves.
|
|
415
|
+
*/
|
|
416
|
+
exit: () => Promise<void>
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function useAnimatePresence(options?: UseAnimatePresenceOptions): UseAnimatePresenceResult {
|
|
420
|
+
const runExits = new Map<MotionElement, () => Promise<void>>()
|
|
421
|
+
const [presenceInitial, setPresenceInitial] = createSignal(options?.initial ?? true)
|
|
422
|
+
// Flip to true after the first microtask so mid-life mounts always animate,
|
|
423
|
+
// regardless of the initial setting.
|
|
424
|
+
queueMicrotask(() => setPresenceInitial(true))
|
|
425
|
+
|
|
426
|
+
const ctx: PresenceContextValue = {
|
|
427
|
+
register: (el, runExit) => {
|
|
428
|
+
runExits.set(el, runExit)
|
|
429
|
+
},
|
|
430
|
+
unregister: (el) => {
|
|
431
|
+
runExits.delete(el)
|
|
432
|
+
},
|
|
433
|
+
beforeUnmount: (el) => {
|
|
434
|
+
// Mirrors `<Presence>`'s subtree-walk semantics (see its
|
|
435
|
+
// beforeUnmount JSDoc). The hook's own `exit()` API instead
|
|
436
|
+
// iterates the full registry directly — but anyone who hands
|
|
437
|
+
// this ctx to a transition-coordinator that calls `beforeUnmount`
|
|
438
|
+
// gets the same descendant-cascade behavior.
|
|
439
|
+
const exiting = collectSubtreeExits(el, runExits)
|
|
440
|
+
if (exiting.length === 0) return Promise.resolve()
|
|
441
|
+
return Promise.all(exiting.map((pair) => pair[1]())).then(() => {
|
|
442
|
+
for (const [exitedEl] of exiting) runExits.delete(exitedEl)
|
|
443
|
+
})
|
|
444
|
+
},
|
|
445
|
+
initial: presenceInitial,
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Free the registry on owner disposal. Motion children deliberately do NOT
|
|
449
|
+
// unregister in their own onCleanup (see ADR 0003 — that would race ahead
|
|
450
|
+
// of the exit window), so when the hook's owner unmounts without ever
|
|
451
|
+
// calling exit(), this is the only path that drops the entries.
|
|
452
|
+
onCleanup(() => runExits.clear())
|
|
453
|
+
|
|
454
|
+
const Provider: Component<{ children: JSX.Element }> = (p) => (
|
|
455
|
+
<PresenceContext.Provider value={ctx}>{p.children}</PresenceContext.Provider>
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
const exit = async (): Promise<void> => {
|
|
459
|
+
// Snapshot the registry at call time so concurrent registrations during
|
|
460
|
+
// the await don't get added to this batch.
|
|
461
|
+
const snapshot = [...runExits.values()]
|
|
462
|
+
await Promise.all(snapshot.map((fn) => fn()))
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return { Provider, exit }
|
|
466
|
+
}
|