framer-motion
Version:
A simple and powerful JavaScript animation library
1 lines • 10.2 kB
Source Map (JSON)
{"version":3,"file":"PopChild.mjs","sources":["../../../../src/components/AnimatePresence/PopChild.tsx"],"sourcesContent":["\"use client\"\n\nimport { isHTMLElement, microtask, type VisualElement } from \"motion-dom\"\nimport * as React from \"react\"\nimport { useContext, useId, useInsertionEffect, useRef } from \"react\"\n\nimport { MotionConfigContext } from \"../../context/MotionConfigContext\"\nimport { useComposedRefs } from \"../../utils/use-composed-ref\"\n\ninterface Size {\n width: number\n height: number\n top: number\n left: number\n right: number\n bottom: number\n direction: string\n point?: number[]\n}\n\ninterface Props {\n children: React.ReactElement\n isPresent: boolean\n anchorX?: \"left\" | \"right\"\n anchorY?: \"top\" | \"bottom\"\n root?: HTMLElement | ShadowRoot\n pop?: boolean\n popNode?: React.RefObject<VisualElement | undefined>\n}\n\ninterface MeasureProps extends Props {\n getElement: () => unknown\n sizeRef: React.RefObject<Size>\n}\n\n/**\n * How much an element's transforms, and its ancestors', scale it on screen.\n */\nconst measureScale = (element?: HTMLElement | null) => {\n const { width, height } =\n element?.getBoundingClientRect() || ({} as DOMRect)\n return [\n width / element?.offsetWidth! || 1,\n height / element?.offsetHeight! || 1,\n ]\n}\n\n/**\n * The element's position in the space it scrolls in, as the projection\n * system measures it (see measurePageBox and removeElementScroll): its\n * viewport position, plus its scroll containers' scroll (across shadow\n * roots), plus the window's (the root's) scroll. Like checkIsScrollRoot, a\n * fixed ancestor is a scroll root: it doesn't scroll with anything above it,\n * so a fixed container is measured relative to the viewport.\n *\n * So a scroll position clamped by content shrinking reads as the element\n * moving only if it doesn't scroll with that content.\n *\n * Scroll offsets are in their container's coordinate space, so are scaled\n * onto the screen, and the result is returned in the offsetParent's\n * coordinate space, which the pop rule's offsets are in.\n */\nconst measurePoint = (element: HTMLElement) => {\n let { left, top } = element.getBoundingClientRect()\n let node: HTMLElement | null | undefined = element\n while (\n (node = (node.parentElement ||\n (node.parentNode as ShadowRoot)?.host) as HTMLElement | null)\n ) {\n const { scrollLeft, scrollTop } = node\n if (scrollLeft || scrollTop) {\n const [x, y] = measureScale(node)\n left += scrollLeft * x\n top += scrollTop * y\n }\n if (getComputedStyle(node).position === \"fixed\") break\n }\n const [x, y] = measureScale(element.offsetParent as HTMLElement | null)\n return [left / x, top / y]\n}\n\n/**\n * Measurement functionality has to be within a separate component\n * to leverage snapshot lifecycle.\n */\nclass PopChildMeasure extends React.Component<MeasureProps> {\n getSnapshotBeforeUpdate(prevProps: MeasureProps) {\n const element = this.props.getElement()\n if (isHTMLElement(element) && prevProps.isPresent && !this.props.isPresent && this.props.pop !== false) {\n const parent = element.offsetParent\n const parentWidth = isHTMLElement(parent)\n ? parent.offsetWidth || 0\n : 0\n const parentHeight = isHTMLElement(parent)\n ? parent.offsetHeight || 0\n : 0\n\n const computedStyle = getComputedStyle(element)\n const size = this.props.sizeRef.current!\n size.height = parseFloat(computedStyle.height)\n size.width = parseFloat(computedStyle.width)\n size.top = element.offsetTop\n size.left = element.offsetLeft\n size.right = parentWidth - size.width - size.left\n size.bottom = parentHeight - size.height - size.top\n size.direction = computedStyle.direction\n size.point = measurePoint(element)\n }\n\n return null\n }\n\n /**\n * Required with getSnapshotBeforeUpdate to stop React complaining.\n */\n componentDidUpdate() {}\n\n render() {\n return this.props.children\n }\n}\n\nexport function PopChild({ children, isPresent, anchorX, anchorY, root, pop, popNode }: Props) {\n const id = useId()\n const ref = useRef<HTMLElement>(null)\n const size = useRef<Size>({\n width: 0,\n height: 0,\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n direction: \"ltr\",\n })\n const { nonce } = useContext(MotionConfigContext)\n /**\n * In React 19, refs are passed via props.ref instead of element.ref.\n * We check props.ref first (React 19) and fall back to element.ref (React 18).\n *\n * The composed ref is only used when popping the child out (pop !== false),\n * so we avoid reading children.props.ref otherwise. In React 18.3 that read\n * triggers a spurious \"`ref` is not a prop\" warning getter (#3745).\n */\n const childRef =\n pop !== false\n ? ((children.props as { ref?: React.Ref<HTMLElement> })?.ref ??\n (children as unknown as { ref?: React.Ref<HTMLElement> })?.ref)\n : undefined\n const composedRef = useComposedRefs(ref, childRef)\n\n /**\n * The child's element: the one our ref reached or, if the child can't\n * be given a ref (for instance a function component that doesn't\n * forward it), its outermost motion component's element (#2503).\n */\n const getElement = () =>\n ref.current || (popNode?.current?.current as HTMLElement | undefined)\n\n const insertPopRule = (sheet: CSSStyleSheet | null) => {\n const { width, height, top, left, right, bottom, direction } = size.current\n const isRTL = direction === \"rtl\"\n const x = anchorX === \"left\"\n ? (isRTL ? `right:${right}` : `left:${left}`)\n : (isRTL ? `left:${left}` : `right:${right}`)\n const y = anchorY === \"bottom\" ? `bottom:${bottom}` : `top:${top}`\n\n sheet?.insertRule(\n `[data-motion-pop-id=\"${id}\"]{position:absolute!important;width:${width}px!important;height:${height}px!important;${x}px!important;${y}px!important}`\n )\n }\n\n /**\n * We create and inject a style block so we can apply this explicit\n * sizing in a non-destructive manner by just deleting the style block.\n *\n * We can't apply size via render as the measurement happens\n * in getSnapshotBeforeUpdate (post-render), likewise if we apply the\n * styles directly on the DOM node, we might be overwriting\n * styles set via the style prop.\n */\n useInsertionEffect(() => {\n const { width, height } = size.current\n const element = getElement()\n if (isPresent || pop === false || !element || !width || !height) return\n\n element.dataset.motionPopId = id\n\n /**\n * Use the element's own document so this works when rendering\n * into another window or iframe (e.g. via window.open()).\n */\n const doc = element.ownerDocument\n const style = doc.createElement(\"style\")\n if (nonce) style.nonce = nonce\n\n const parent = root ?? doc.head\n parent.appendChild(style)\n\n insertPopRule(style.sheet)\n\n /**\n * Popping the element out of the layout can move its offsetParent,\n * which we've pinned it to. For instance, a bottom-anchored or\n * centred container shrinks around its remaining children (#2348).\n * So once the whole commit is in the DOM, measure how far the\n * element moved and offset it back.\n *\n * This runs on the microtask frameloop, so before paint even if\n * this commit happens within an animation frame. Every popped\n * element is measured in its first step, then corrected at the\n * start of the read step: before the projection system, which\n * schedules its layout measurement there from layout effects, so\n * layout animations see the corrected position.\n */\n let dx = 0\n let dy = 0\n microtask.setup(() => {\n const [x, y] = measurePoint(element)\n dx = size.current.point![0] - x\n dy = size.current.point![1] - y\n })\n microtask.read(() => {\n /**\n * The style has no sheet if it's been removed since, because\n * the element is present again.\n */\n const { sheet } = style\n if (!sheet || (!dx && !dy)) return\n const s = size.current\n s.left += dx\n s.right -= dx\n s.top += dy\n s.bottom -= dy\n sheet.deleteRule(0)\n insertPopRule(sheet)\n })\n\n return () => {\n element.removeAttribute(\"data-motion-pop-id\")\n if (parent.contains(style)) {\n parent.removeChild(style)\n }\n }\n }, [isPresent])\n\n return (\n <PopChildMeasure isPresent={isPresent} getElement={getElement} sizeRef={size} pop={pop}>\n {pop === false\n ? children\n : React.cloneElement(children as any, { ref: composedRef })}\n </PopChildMeasure>\n )\n}\n"],"names":[],"mappings":";;;;;;;;AAmCA;;AAEG;AACH;AACI;;AAGI;AACA;;AAER;AAEA;;;;;;;;;;;;;;AAcG;AACH;;;AAGI;AAES;AAEL;AACA;;AAEI;AACA;;AAEJ;;;AAEJ;;AAEJ;AAEA;;;AAGG;AACH;AACI;;;AAGQ;AACA;AACI;;AAEJ;AACI;;AAGJ;;;;AAIA;AACA;AACA;AACA;AACA;AACA;;AAGJ;;AAGJ;;AAEG;AACH;;AAGI;;AAEP;;AAGG;AACA;;AAEI;AACA;AACA;AACA;AACA;AACA;AACA;AACH;;AAED;;;;;;;AAOG;AACH;AAEQ;;;;AAKR;;;;AAIG;AACH;AAGA;AACI;AACA;AACA;AACI;AACA;AACJ;AAEA;AAGJ;AAEA;;;;;;;;AAQG;;;AAGC;AACA;;AAEA;AAEA;;;AAGG;AACH;;AAEA;AAAW;AAEX;AACA;AAEA;AAEA;;;;;;;;;;;;;AAaG;;;AAGH;;;;AAIA;AACA;AACI;;;AAGG;AACH;;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEJ;AAEA;AACI;AACA;AACI;;AAER;AACJ;;AAKY;AACA;AAGhB;;"}