Skip to content

Commit

Permalink
gets rid of legacies within animatepresence and unifiess default expo…
Browse files Browse the repository at this point in the history
…rt ⚙️
  • Loading branch information
cliffordkleinsr committed Dec 18, 2024
1 parent 5714f68 commit 9dd5f66
Show file tree
Hide file tree
Showing 10 changed files with 1,120 additions and 405 deletions.
816 changes: 816 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

Binary file removed bun.lockb
Binary file not shown.
266 changes: 142 additions & 124 deletions src/lib/motion-start/components/AnimatePresence/AnimatePresence.svelte
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}
Loading

0 comments on commit 9dd5f66

Please sign in to comment.