framer-motion
Version:
A simple and powerful JavaScript animation library
192 lines (189 loc) • 7.67 kB
JavaScript
"use client";
import { jsx } from 'react/jsx-runtime';
import { globalProjectionState, frame, microtask } from 'motion-dom';
import { useContext, Component } from 'react';
import { usePresence } from '../../../components/AnimatePresence/use-presence.mjs';
import { LayoutGroupContext } from '../../../context/LayoutGroupContext.mjs';
import { SwitchLayoutGroupContext } from '../../../context/SwitchLayoutGroupContext.mjs';
/**
* Track whether we've taken any snapshots yet. If not,
* we can safely skip notification of didUpdate.
*
* Difficult to capture in a test but to prevent flickering
* we must set this to true either on update or unmount.
* Running `next-env/layout-id` in Safari will show this behaviour if broken.
*/
let hasTakenAnySnapshot = false;
/**
* The projection node is created with the props from the component's first
* render, so keep these in sync as they can change (or be enabled) later.
*/
const layoutOptions = ({ layout, layoutDependency, layoutAnchor, }) => ({
layout,
layoutDependency,
layoutAnchor,
animationType: typeof layout === "string" ? layout : "both",
});
/**
* Move a mounted node to its new layoutId's stack, as an unmount and remount
* would. Returns a takeover to run once every node in the commit has had the
* chance to change its layoutId.
*/
const switchLayoutId = (projection, layoutId) => {
const { snapshot } = projection;
const stack = projection.getStack();
stack?.remove(projection);
/**
* If this node led the old stack (so remove() made it the prevLead of
* the new lead), the new lead animates from this node's snapshot, which
* it now holds. Unlike a node that unmounts, this one stays mounted, so
* unlink them: the new lead mustn't read this node's snapshot after it
* changes, or stop or share its animation.
*/
const lead = stack?.lead;
if (stack?.prevLead === projection && lead.resumeFrom === projection)
lead.resumeFrom = lead.currentAnimation = projection.resumingFrom =
undefined;
projection.options.layoutId = layoutId;
/**
* A node that followed another no longer does, so clear the styles
* that hid it.
*/
if (!layoutId) {
projection.needsShow = true;
return;
}
projection.root.registerSharedNode(layoutId, projection);
/**
* Like a mounting node, this one animates from the lead it replaces.
* But if that lead is still on screen with this layoutId (e.g. an open
* card moving to another thumbnail), this node is already on screen
* too, so it takes over instantly: it keeps its own snapshot, and the
* lead is hidden, as at the end of a crossfade. A lead that left the
* stack in this commit (e.g. layoutId={selected ? "x" : undefined})
* is animated from, as is one that's exiting.
*/
return () => {
const { resumeFrom } = projection;
if (resumeFrom?.isPresent !== false &&
projection.getStack().members.includes(resumeFrom)) {
resumeFrom.hide();
projection.resumeFrom = undefined;
projection.snapshot = snapshot;
}
};
};
class MeasureLayoutWithContext extends Component {
/**
* This only mounts projection nodes for components that
* need measuring, we might want to do it for all components
* in order to incorporate transforms
*/
componentDidMount() {
const { visualElement, layoutGroup, switchLayoutGroup, layoutId } = this.props;
const { projection } = visualElement;
if (projection) {
layoutGroup.group?.add(projection);
layoutId && switchLayoutGroup?.register?.(projection);
if (hasTakenAnySnapshot) {
projection.root.didUpdate();
}
/**
* Without `layout` or `drag`, this only renders while there's a
* layoutId, so a mounted node can gain one here.
*/
if (projection.options.layoutId !== layoutId)
switchLayoutId(projection, layoutId)?.();
projection.addEventListener("animationComplete", () => this.safeToRemove());
projection.setOptions({
...projection.options,
...layoutOptions(this.props),
onExitComplete: () => this.safeToRemove(),
});
}
globalProjectionState.hasEverUpdated = true;
}
getSnapshotBeforeUpdate(prevProps) {
const { layoutDependency, layoutId, visualElement, drag, isPresent } = this.props;
const { projection } = visualElement;
if (!projection)
return null;
/**
* TODO: We use this data in relegate to determine whether to
* promote a previous element. There's no guarantee its presence data
* will have updated by this point - if a bug like this arises it will
* have to be that we markForRelegation and then find a new lead some other way,
* perhaps in didUpdate
*/
projection.isPresent = isPresent;
this.takeOver =
prevProps.layoutId !== layoutId
? (projection.willUpdate(), switchLayoutId(projection, layoutId))
: undefined;
projection.setOptions({
...projection.options,
...layoutOptions(this.props),
});
hasTakenAnySnapshot = true;
if (drag ||
prevProps.layoutDependency !== layoutDependency ||
layoutDependency === undefined ||
prevProps.isPresent !== isPresent) {
projection.willUpdate();
}
else {
this.safeToRemove();
}
if (prevProps.isPresent !== isPresent) {
if (isPresent) {
projection.promote();
}
else if (!projection.relegate()) {
/**
* If there's another stack member taking over from this one,
* it's in charge of the exit animation and therefore should
* be in charge of the safe to remove. Otherwise we call it here.
*/
frame.postRender(() => {
projection.getStack()?.members.length || this.safeToRemove();
});
}
}
return null;
}
componentDidUpdate() {
const { projection } = this.props.visualElement;
if (projection) {
this.takeOver?.();
projection.root.didUpdate();
microtask.postRender(() => {
if (!projection.currentAnimation && projection.isLead()) {
this.safeToRemove();
}
});
}
}
componentWillUnmount() {
const { visualElement, layoutGroup, switchLayoutGroup: promoteContext, } = this.props;
const { projection } = visualElement;
hasTakenAnySnapshot = true;
if (projection) {
projection.scheduleCheckAfterUnmount();
layoutGroup.group?.remove(projection);
promoteContext?.deregister?.(projection);
}
}
safeToRemove() {
this.props.safeToRemove?.();
}
render() {
return null;
}
}
function MeasureLayout(props) {
const [isPresent, safeToRemove] = usePresence();
const layoutGroup = useContext(LayoutGroupContext);
return (jsx(MeasureLayoutWithContext, { ...props, layoutGroup: layoutGroup, switchLayoutGroup: useContext(SwitchLayoutGroupContext), isPresent: isPresent, safeToRemove: safeToRemove }));
}
export { MeasureLayout };
//# sourceMappingURL=MeasureLayout.mjs.map