Skip to content

Commit

Permalink
Restore Legacy animatepresence stack with conditional ✅
Browse files Browse the repository at this point in the history
  • Loading branch information
cliffordkleinsr committed Dec 17, 2024
1 parent 6f8cdb6 commit 5714f68
Show file tree
Hide file tree
Showing 11 changed files with 363 additions and 2 deletions.
Binary file modified bun.lockb
Binary file not shown.
23 changes: 21 additions & 2 deletions src/lib/components/motion/AnimatePresenceStack.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
<script>
import Box from "../Box.svelte";
import { AnimatePresence } from "$lib/motion-start";
import { Label } from "$lib/components/ui/label";
import { Switch } from "$lib/components/ui/switch";
import { AnimatePresence, AnimatePresenceLegacy } from "$lib/motion-start";
import Card from "$lib/components/Card.svelte";
let index = 0;
let use_legacy= false
$: mint = index + 1;
</script>

<div class="flex items-center space-x-2">
<Switch id="airplane-mode" bind:checked={use_legacy}/>
<Label for="airplane-mode">Use Legacy</Label>
</div>
{#if use_legacy}
<Box>
<div
class="w-64 h-64 relative bg-gray-700/40 rounded-lg flex justify-center items-center"
>
<AnimatePresenceLegacy initial={false} list={[{ key: index }]}>
<Card bind:index={mint} frontCard={false} />
<Card bind:index drag="x" frontCard={true} />
</AnimatePresenceLegacy>
</div>
</Box>
{:else}
<Box>
<div
class="w-64 h-64 relative bg-gray-700/40 rounded-lg flex justify-center items-center"
Expand All @@ -16,3 +34,4 @@
</AnimatePresence>
</div>
</Box>
{/if}
7 changes: 7 additions & 0 deletions src/lib/components/ui/label/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Root from "./label.svelte";

export {
Root,
//
Root as Label,
};
21 changes: 21 additions & 0 deletions src/lib/components/ui/label/label.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
let className: $$Props["class"] = undefined;
export { className as class };
</script>

<LabelPrimitive.Root
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}
on:mousedown
>
<slot />
</LabelPrimitive.Root>
7 changes: 7 additions & 0 deletions src/lib/components/ui/switch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Root from "./switch.svelte";

export {
Root,
//
Root as Switch,
};
28 changes: 28 additions & 0 deletions src/lib/components/ui/switch/switch.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import { Switch as SwitchPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = SwitchPrimitive.Props;
type $$Events = SwitchPrimitive.Events;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
</script>

<SwitchPrimitive.Root
bind:checked
class={cn(
"focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
>
<SwitchPrimitive.Thumb
class={cn(
"bg-background pointer-events-none block h-5 w-5 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<!-- based on [email protected],
Copyright (c) 2018 Framer B.V. -->

<script lang="ts" generics="T extends {key:any}">
import type { ConditionalGeneric, AnimatePresenceProps } from "./index.js";
import { getContext } from "svelte";
import PresenceChild from "./PresenceChild/PresenceChild.svelte";
import { writable, type Writable } from "svelte/store";
//@ts-expect-error
import { isSharedLayout, SharedLayoutContext } from "./legacies/SharedLayoutContext.js";
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,
show: $$Props["show"] = undefined,
isCustom = false;
let _list = list !== undefined ? list : show ? [{ key: 1 }] : [];
$: _list = list !== undefined ? list : show ? [{ key: 1 }] : [];
const layoutContext =
getContext<Writable<any>>(
SharedLayoutContext,
) || SharedLayoutContext(isCustom);
$: forceRender = () => {
if (isSharedLayout($layoutContext)) {
$layoutContext.forceUpdate();
}
_list = [..._list];
};
function getChildKey(child: { key: number }) {
return child.key || "";
}
let isInitialRender = true;
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);
}
}
// 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 child = allChildren.get(key);
if (!child) return;
const insertionIndex = presentKeys.indexOf(key);
const onExit = () => {
allChildren.delete(key);
exiting.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 (!exiting.size) {
presentChildren = [...filteredChildren];
forceRender();
onExitComplete && onExitComplete();
}
};
childrenToRender.splice(insertionIndex, 0, {
present: false,
item: child,
key: getChildKey(child),
onExit,
});
});
// 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 childrenToRender as child (getChildKey(child))}
<PresenceChild
mode="sync"
isPresent={child.present}
initial={initial ? undefined : false}
custom={child.onExit ? custom : undefined}
{presenceAffectsLayout}
onExitComplete={child.onExit}
{isCustom}
>
<slot item={child.item} />
</PresenceChild>
{/each}
1 change: 1 addition & 0 deletions src/lib/motion-start/components/AnimatePresence/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ export type { AnimatePresenceProps } from "./types.js";
*/
export type ConditionalGeneric<T> = T extends {key:any} ? T : { key: 1}; // Better handling of defaults and the optional list prop
export { default as AnimatePresence } from './AnimatePresence.svelte';
export { default as AnimatePresenceLegacy } from './AnimatePresenceLegacy.svelte';
export { PresenceChild } from './PresenceChild/index.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

//"$lib/motion-start/context/DOMcontext.js";
import { writable } from "svelte/store";
export const getDomContext = (
name,
el
) => {
if (!el || !window) {
return undefined;
}
let par = el;
while ((par = par.parentNode)) {
if (par.motionDomContext && par.motionDomContext.has(name)) {
return par.motionDomContext.get(name);
}
}
return undefined;
};
import { createBatcher } from "./batcher";
function SharedLayoutContext(custom) {
return (getDomContext('SharedLayout', custom)) || writable(createBatcher());
}
function isSharedLayout(context){
return 'forceUpdate' in context;
}

export { SharedLayoutContext, isSharedLayout };
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
function createBatcher() {
var queue = new Set();
return {
add: (child) => queue.add(child),
flush: (_a) => {
var _b = _a === void 0 ? defaultHandler : _a,
layoutReady = _b.layoutReady,
parent = _b.parent;
batchLayout((read, write) => {
var order = Array.from(queue).sort(compareByDepth);
var ancestors = parent ? collectProjectingAncestors(parent) : [];
write(() => {
var allElements = [...ancestors, ...order];
allElements.forEach((element) => element.resetTransform());
});
read(() => {

order.forEach(updateLayoutMeasurement);
});
write(() => {
ancestors.forEach((element) => element.restoreTransform());

order.forEach(layoutReady);
});
read(() => {
/**
* After all children have started animating, ensure any Entering components are set to Present.
* If we add deferred animations (set up all animations and then start them in two loops) this
* could be moved to the start loop. But it needs to happen after all the animations configs
* are generated in AnimateSharedLayout as this relies on presence data
*/
order.forEach((child) => {
if (child.isPresent) child.presence = Presence.Present;
});
});
write(() => {
/**
* Starting these animations will have queued jobs on the frame loop. In some situations,
* like when removing an element, these will be processed too late after the DOM is manipulated,
* leaving a flash of incorrectly-projected content. By manually flushing these jobs
* we ensure there's no flash.
*/
flushSync.preRender();
flushSync.render();
});
read(() => {
/**
* Schedule a callback at the end of the following frame to assign the latest projection
* box to the prevViewportBox snapshot. Once global batching is in place this could be run
* synchronously. But for now it ensures that if any nested `AnimateSharedLayout` top-level
* child attempts to calculate its previous relative position against a prevViewportBox
* it will be against its latest projection box instead, as the snapshot is useless beyond this
* render.
*/

sync.postRender(() => order.forEach(assignProjectionToSnapshot));
queue.clear();
});
});
// TODO: Need to find a layout-synchronous way of flushing this
flushLayout();
},
};
}
export { createBatcher };
Loading

0 comments on commit 5714f68

Please sign in to comment.