UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

1 lines • 9.27 kB
{"version":3,"file":"MeasureLayout.mjs","sources":["../../../../../src/motion/features/layout/MeasureLayout.tsx"],"sourcesContent":["\"use client\"\n\nimport {\n frame,\n microtask,\n globalProjectionState,\n type IProjectionNode,\n type VisualElement,\n} from \"motion-dom\"\nimport { Component, useContext } from \"react\"\nimport { usePresence } from \"../../../components/AnimatePresence/use-presence\"\nimport {\n LayoutGroupContext,\n LayoutGroupContextProps,\n} from \"../../../context/LayoutGroupContext\"\nimport { SwitchLayoutGroupContext } from \"../../../context/SwitchLayoutGroupContext\"\nimport { MotionProps } from \"../../types\"\n\ninterface MeasureContextProps {\n layoutGroup: LayoutGroupContextProps\n switchLayoutGroup?: SwitchLayoutGroupContext\n isPresent: boolean\n safeToRemove?: VoidFunction | null\n}\n\ntype MeasureProps = MotionProps &\n MeasureContextProps & { visualElement: VisualElement }\n\n/**\n * Track whether we've taken any snapshots yet. If not,\n * we can safely skip notification of didUpdate.\n *\n * Difficult to capture in a test but to prevent flickering\n * we must set this to true either on update or unmount.\n * Running `next-env/layout-id` in Safari will show this behaviour if broken.\n */\nlet hasTakenAnySnapshot = false\n\n/**\n * The projection node is created with the props from the component's first\n * render, so keep these in sync as they can change (or be enabled) later.\n */\nconst layoutOptions = ({\n layout,\n layoutDependency,\n layoutAnchor,\n}: MeasureProps) => ({\n layout,\n layoutDependency,\n layoutAnchor,\n animationType: typeof layout === \"string\" ? layout : \"both\",\n})\n\n/**\n * Move a mounted node to its new layoutId's stack, as an unmount and remount\n * would. Returns a takeover to run once every node in the commit has had the\n * chance to change its layoutId.\n */\nconst switchLayoutId = (projection: IProjectionNode, layoutId?: string) => {\n const { snapshot } = projection\n const stack = projection.getStack()\n stack?.remove(projection)\n /**\n * If this node led the old stack (so remove() made it the prevLead of\n * the new lead), the new lead animates from this node's snapshot, which\n * it now holds. Unlike a node that unmounts, this one stays mounted, so\n * unlink them: the new lead mustn't read this node's snapshot after it\n * changes, or stop or share its animation.\n */\n const lead = stack?.lead\n if (stack?.prevLead === projection && lead!.resumeFrom === projection)\n lead!.resumeFrom = lead!.currentAnimation = projection.resumingFrom =\n undefined\n projection.options.layoutId = layoutId\n\n /**\n * A node that followed another no longer does, so clear the styles\n * that hid it.\n */\n if (!layoutId) {\n projection.needsShow = true\n return\n }\n projection.root!.registerSharedNode(layoutId, projection)\n\n /**\n * Like a mounting node, this one animates from the lead it replaces.\n * But if that lead is still on screen with this layoutId (e.g. an open\n * card moving to another thumbnail), this node is already on screen\n * too, so it takes over instantly: it keeps its own snapshot, and the\n * lead is hidden, as at the end of a crossfade. A lead that left the\n * stack in this commit (e.g. layoutId={selected ? \"x\" : undefined})\n * is animated from, as is one that's exiting.\n */\n return () => {\n const { resumeFrom } = projection\n if (\n resumeFrom?.isPresent !== false &&\n projection.getStack()!.members.includes(resumeFrom!)\n ) {\n resumeFrom!.hide()\n projection.resumeFrom = undefined\n projection.snapshot = snapshot\n }\n }\n}\n\nclass MeasureLayoutWithContext extends Component<MeasureProps> {\n takeOver?: VoidFunction\n\n /**\n * This only mounts projection nodes for components that\n * need measuring, we might want to do it for all components\n * in order to incorporate transforms\n */\n componentDidMount() {\n const { visualElement, layoutGroup, switchLayoutGroup, layoutId } =\n this.props\n const { projection } = visualElement\n\n if (projection) {\n layoutGroup.group?.add(projection)\n layoutId && switchLayoutGroup?.register?.(projection)\n\n if (hasTakenAnySnapshot) {\n projection.root!.didUpdate()\n }\n\n /**\n * Without `layout` or `drag`, this only renders while there's a\n * layoutId, so a mounted node can gain one here.\n */\n if (projection.options.layoutId !== layoutId)\n switchLayoutId(projection, layoutId)?.()\n\n projection.addEventListener(\"animationComplete\", () =>\n this.safeToRemove()\n )\n projection.setOptions({\n ...projection.options,\n ...layoutOptions(this.props),\n onExitComplete: () => this.safeToRemove(),\n })\n }\n\n globalProjectionState.hasEverUpdated = true\n }\n\n getSnapshotBeforeUpdate(prevProps: MeasureProps) {\n const { layoutDependency, layoutId, visualElement, drag, isPresent } =\n this.props\n const { projection } = visualElement\n\n if (!projection) return null\n\n /**\n * TODO: We use this data in relegate to determine whether to\n * promote a previous element. There's no guarantee its presence data\n * will have updated by this point - if a bug like this arises it will\n * have to be that we markForRelegation and then find a new lead some other way,\n * perhaps in didUpdate\n */\n projection.isPresent = isPresent\n\n this.takeOver =\n prevProps.layoutId !== layoutId\n ? (projection.willUpdate(), switchLayoutId(projection, layoutId))\n : undefined\n\n projection.setOptions({\n ...projection.options,\n ...layoutOptions(this.props),\n })\n\n hasTakenAnySnapshot = true\n\n if (\n drag ||\n prevProps.layoutDependency !== layoutDependency ||\n layoutDependency === undefined ||\n prevProps.isPresent !== isPresent\n ) {\n projection.willUpdate()\n } else {\n this.safeToRemove()\n }\n\n if (prevProps.isPresent !== isPresent) {\n if (isPresent) {\n projection.promote()\n } else if (!projection.relegate()) {\n /**\n * If there's another stack member taking over from this one,\n * it's in charge of the exit animation and therefore should\n * be in charge of the safe to remove. Otherwise we call it here.\n */\n frame.postRender(() => {\n projection.getStack()?.members.length || this.safeToRemove()\n })\n }\n }\n\n return null\n }\n\n componentDidUpdate() {\n const { projection } = this.props.visualElement\n if (projection) {\n this.takeOver?.()\n projection.root!.didUpdate()\n\n microtask.postRender(() => {\n if (!projection.currentAnimation && projection.isLead()) {\n this.safeToRemove()\n }\n })\n }\n }\n\n componentWillUnmount() {\n const {\n visualElement,\n layoutGroup,\n switchLayoutGroup: promoteContext,\n } = this.props\n const { projection } = visualElement\n\n hasTakenAnySnapshot = true\n\n if (projection) {\n projection.scheduleCheckAfterUnmount()\n layoutGroup.group?.remove(projection)\n promoteContext?.deregister?.(projection)\n }\n }\n\n safeToRemove() {\n this.props.safeToRemove?.()\n }\n\n render() {\n return null\n }\n}\n\nexport function MeasureLayout(\n props: MotionProps & { visualElement: VisualElement }\n) {\n const [isPresent, safeToRemove] = usePresence()\n const layoutGroup = useContext(LayoutGroupContext)\n\n return (\n <MeasureLayoutWithContext\n {...props}\n layoutGroup={layoutGroup}\n switchLayoutGroup={useContext(SwitchLayoutGroupContext)}\n isPresent={isPresent}\n safeToRemove={safeToRemove}\n />\n )\n}\n"],"names":[],"mappings":";;;;;;;;AA4BA;;;;;;;AAOG;AACH;AAEA;;;AAGG;AACH;;;;AAQI;AACH;AAED;;;;AAIG;AACH;AACI;AACA;AACA;AACA;;;;;;AAMG;AACH;;;AAGQ;AACR;AAEA;;;AAGG;;AAEC;;;;AAKJ;;;;;;;;AAQG;AACH;AACI;AACA;;;AAKI;AACA;;AAER;AACJ;AAEA;AAGI;;;;AAIG;;AAEC;AAEA;;AAGI;;;AAII;;AAGJ;;;AAGG;AACH;AACI;AAEJ;;;AAKI;AACA;AACH;;AAGL;;AAGJ;AACI;AAEA;AAEA;AAAiB;AAEjB;;;;;;AAMG;AACH;AAEA;;AAEQ;;;;AAKJ;AACH;;AAID;;AAGI;AACA;;;;;;AAOJ;;;;AAGW;AACH;;;;AAIG;AACH;AACI;AACJ;;;AAIR;;;;;AAMI;AACA;AAEA;;;;AAIA;;;;AAKJ;AAKA;;;;AAMI;AACA;;;;AAKJ;;;AAIA;;AAEP;AAEK;;AAIF;;AAWJ;;"}