-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
gets rid of legacies within animatepresence and unifiess default expo…
…rt ⚙️
- Loading branch information
1 parent
5714f68
commit 9dd5f66
Showing
10 changed files
with
1,120 additions
and
405 deletions.
There are no files selected for viewing
266 changes: 142 additions & 124 deletions
266
src/lib/motion-start/components/AnimatePresence/AnimatePresence.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,160 +1,178 @@ | ||
<!-- based on framer-motion@11.11.11, | ||
<!-- based on framer-motion@4.0.3, | ||
Copyright (c) 2018 Framer B.V. --> | ||
<svelte:options runes /> | ||
|
||
<script lang="ts" generics="T extends {key:any}"> | ||
import { Previous } from "runed"; | ||
import type { ConditionalGeneric, AnimatePresenceProps } from "./index.js"; | ||
import { useContext } from "../../context/utils/context.svelte.js"; | ||
import { LayoutGroupContext } from "../../context/LayoutGroupContext"; | ||
import { getContext } from "svelte"; | ||
import PresenceChild from "./PresenceChild/PresenceChild.svelte"; | ||
import { useContext } from "../../context/utils/context.svelte.js"; | ||
import { LayoutGroupContext } from "../../context/LayoutGroupContext.js"; | ||
import { fromStore } from "svelte/store"; | ||
import { SvelteMap, SvelteSet } from "svelte/reactivity"; | ||
import { onDestroy, onMount, untrack, type Snippet } from "svelte"; | ||
import { type ComponentKey, getChildKey } from "./utils.js"; | ||
import { invariant } from "../../utils/errors.js"; | ||
type Props = AnimatePresenceProps<ConditionalGeneric<T>> & { | ||
children?: Snippet<[typeof list | { key: number }]>; | ||
}; | ||
let { | ||
exitBeforeEnter, | ||
custom, | ||
initial, | ||
onExitComplete, | ||
type $$Props = AnimatePresenceProps<ConditionalGeneric<T>>; | ||
export let list: $$Props["list"] = undefined, | ||
custom: $$Props["custom"] = undefined, | ||
initial: $$Props["initial"] = true, | ||
onExitComplete: $$Props["onExitComplete"] = undefined, | ||
exitBeforeEnter: $$Props["exitBeforeEnter"] = undefined, | ||
presenceAffectsLayout = true, | ||
mode = "sync", | ||
list, | ||
show, | ||
children, | ||
}: Props = $props(); | ||
show: $$Props["show"] = undefined, | ||
isCustom = false; | ||
let _list = list !== undefined ? list : show ? [{ key: 1 }] : []; | ||
$: _list = list !== undefined ? list : show ? [{ key: 1 }] : []; | ||
invariant(!exitBeforeEnter, "Replace exitBeforeEnter with mode='wait'"); | ||
const layoutContext = fromStore(useContext(LayoutGroupContext)); | ||
$: forceRender = () => { | ||
layoutContext.current.forceRender?.(); | ||
_list = [..._list]; | ||
}; | ||
let _list = $state(list !== undefined ? list : show ? [{ key: 1 }] : []); | ||
let presentChildren = $derived(_list); | ||
let presentKeys = $derived(presentChildren.map(getChildKey)); | ||
function getChildKey(child: { key: number }) { | ||
return child.key || ""; | ||
} | ||
let isInitialRender = true; | ||
let renderedChildren = $derived([ | ||
...presentChildren.map((v) => ({ | ||
let filteredChildren = _list; | ||
$: filteredChildren = _list; | ||
let presentChildren = filteredChildren; | ||
let allChildren = new Map<string | number, { key: number }>(); | ||
let exiting = new Set<"" | number>(); | ||
const updateChildLookup = ( | ||
children: { key: number }[], | ||
allChild: Map<string | number, { key: number }>, | ||
) => { | ||
children.forEach((child) => { | ||
const key = getChildKey(child); | ||
allChild.set(key, child); | ||
}); | ||
}; | ||
$: updateChildLookup(filteredChildren, allChildren); | ||
let childrenToRender: { | ||
present: boolean; | ||
item: any; | ||
key: any; | ||
onExit: undefined | (() => void); | ||
}[] = [ | ||
...filteredChildren.map((v) => ({ | ||
present: true, | ||
item: v, | ||
key: v.key, | ||
onExit: undefined, | ||
})), | ||
]); | ||
]; | ||
$: if (!isInitialRender) { | ||
// If this is a subsequent render, deal with entering and exiting children | ||
childrenToRender = [ | ||
...filteredChildren.map((v) => ({ | ||
present: true, | ||
item: v, | ||
key: v.key, | ||
onExit: undefined, | ||
})), | ||
]; | ||
// Diff the keys of the currently-present and target children to update our | ||
// exiting list. | ||
const presentKeys = presentChildren.map(getChildKey); | ||
const targetKeys = filteredChildren.map(getChildKey); | ||
// Diff the present children with our target children and mark those that are exiting | ||
const numPresent = presentKeys.length; | ||
for (let i = 0; i < numPresent; i++) { | ||
const key = presentKeys[i]; | ||
if (targetKeys.indexOf(key) === -1) { | ||
exiting.add(key); | ||
} else { | ||
// In case this key has re-entered, remove from the exiting list | ||
exiting.delete(key); | ||
} | ||
} | ||
let exitingChildren = new Previous(() => | ||
renderedChildren.map((v) => ({ ...v, present: false })), | ||
); | ||
// If we currently have exiting children, and we're deferring rendering incoming children | ||
// until after all current children have exiting, empty the childrenToRender array | ||
if (exitBeforeEnter && exiting.size) { | ||
childrenToRender = []; | ||
} | ||
// Loop through all currently exiting components and clone them to overwrite `animate` | ||
// with any `exit` prop they might have defined. | ||
exiting.forEach((key) => { | ||
// If this component is actually entering again, early return | ||
if (targetKeys.indexOf(key) !== -1) return; | ||
const exitingKeys = new SvelteSet<number>(); | ||
const child = allChildren.get(key); | ||
if (!child) return; | ||
const layoutContext = fromStore(useContext(LayoutGroupContext)); | ||
const forceRender = () => { | ||
layoutContext.current.forceRender?.(); | ||
_list = [..._list]; | ||
}; | ||
const insertionIndex = presentKeys.indexOf(key); | ||
$inspect(exitingChildren); | ||
const onExit = () => { | ||
allChildren.delete(key); | ||
exiting.delete(key); | ||
onMount(() => { | ||
isInitialRender = false; | ||
}); | ||
$effect.pre(() => { | ||
if ( | ||
!isInitialRender && | ||
exitingChildren.current && | ||
exitingChildren.current.length !== 0 | ||
) { | ||
let nextChildren = [...presentChildren]; | ||
/** | ||
* Update complete status of exiting children. | ||
*/ | ||
for (let i = 0; i < _list.length; i++) { | ||
const child = renderedChildren[i]; | ||
const key = _list[i].key; | ||
if (!presentKeys.includes(key)) { | ||
nextChildren.splice(i, 0, child); | ||
exitingKeys.add(key!); | ||
// Remove this child from the present children | ||
const removeIndex = presentChildren.findIndex( | ||
(presentChild) => presentChild.key === key, | ||
); | ||
if (removeIndex < 0) { | ||
return; | ||
} | ||
} | ||
presentChildren.splice(removeIndex, 1); | ||
/** | ||
* If we're in "wait" mode, and we have exiting children, we want to | ||
* only render these until they've all exited. | ||
*/ | ||
if (mode === "wait" && exitingKeys.size) { | ||
renderedChildren.length = 0; | ||
} | ||
// Defer re-rendering until all exiting children have indeed left | ||
if (!exiting.size) { | ||
presentChildren = [...filteredChildren]; | ||
forceRender(); | ||
onExitComplete && onExitComplete(); | ||
} | ||
}; | ||
exitingKeys.forEach((key) => { | ||
const child = nextChildren.find((e) => e.key === key); | ||
renderedChildren.splice(presentKeys.indexOf(key), 0, { | ||
present: false, | ||
item: child, | ||
key: getChildKey(child), | ||
onExit: | ||
exitingKeys.has(key) && child | ||
? () => { | ||
exitingChildren.delete(key!); | ||
// Remove this child from the present children | ||
const removeIndex = presentChildren.findIndex( | ||
(presentChild) => | ||
presentChild.key === key, | ||
); | ||
if (removeIndex < 0) { | ||
return; | ||
} | ||
presentChildren.splice(removeIndex, 1); | ||
// Defer re-rendering until all exiting children have indeed left | ||
if (!exitingChildren.size) { | ||
_list = [..._list]; | ||
forceRender?.(); | ||
onExitComplete && onExitComplete(); | ||
} | ||
} | ||
: undefined, | ||
}); | ||
childrenToRender.splice(insertionIndex, 0, { | ||
present: false, | ||
item: child, | ||
key: getChildKey(child), | ||
onExit, | ||
}); | ||
/** | ||
* Early return to ensure once we've set state with the latest diffed | ||
* children, we can immediately re-render. | ||
*/ | ||
// untrack(() => (renderedChildren = nextChildren)); | ||
} | ||
}); | ||
if ( | ||
process.env.NODE_ENV !== "production" && | ||
mode === "wait" && | ||
renderedChildren.length > 1 | ||
) { | ||
console.warn( | ||
`You're attempting to animate multiple children within AnimatePresence, but its mode is set to "wait". This will lead to odd visual behaviour.`, | ||
); | ||
}); | ||
// Add `MotionContext` even to children that don't need it to ensure we're rendering | ||
// the same tree between renders | ||
/* | ||
childrenToRender = childrenToRender.map((child) => { | ||
const key = child.key as string | number; | ||
return exiting.has(key) ? ( | ||
child | ||
) : ( | ||
<PresenceChild | ||
key={getChildKey(child)} | ||
isPresent | ||
presenceAffectsLayout={presenceAffectsLayout} | ||
> | ||
{child} | ||
</PresenceChild> | ||
); | ||
}); | ||
*/ | ||
presentChildren = childrenToRender; | ||
} else { | ||
isInitialRender = false; | ||
} | ||
</script> | ||
|
||
{#each renderedChildren as child (getChildKey(child))} | ||
{#each childrenToRender as child (getChildKey(child))} | ||
<PresenceChild | ||
{mode} | ||
mode="sync" | ||
isPresent={child.present} | ||
initial={!isInitialRender || initial ? undefined : false} | ||
initial={initial ? undefined : false} | ||
custom={child.onExit ? custom : undefined} | ||
{presenceAffectsLayout} | ||
onExitComplete={child.onExit} | ||
{isCustom} | ||
> | ||
{@render children?.(child.item)} | ||
<slot item={child.item} /> | ||
</PresenceChild> | ||
{/each} | ||
{/each} |
Oops, something went wrong.