UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

205 lines (202 loc) • 8.36 kB
"use client"; import { jsx } from 'react/jsx-runtime'; import { microtask, isHTMLElement } from 'motion-dom'; import * as React from 'react'; import { useId, useRef, useContext, useInsertionEffect } from 'react'; import { MotionConfigContext } from '../../context/MotionConfigContext.mjs'; import { useComposedRefs } from '../../utils/use-composed-ref.mjs'; /** * How much an element's transforms, and its ancestors', scale it on screen. */ const measureScale = (element) => { const { width, height } = element?.getBoundingClientRect() || {}; return [ width / element?.offsetWidth || 1, height / element?.offsetHeight || 1, ]; }; /** * The element's position in the space it scrolls in, as the projection * system measures it (see measurePageBox and removeElementScroll): its * viewport position, plus its scroll containers' scroll (across shadow * roots), plus the window's (the root's) scroll. Like checkIsScrollRoot, a * fixed ancestor is a scroll root: it doesn't scroll with anything above it, * so a fixed container is measured relative to the viewport. * * So a scroll position clamped by content shrinking reads as the element * moving only if it doesn't scroll with that content. * * Scroll offsets are in their container's coordinate space, so are scaled * onto the screen, and the result is returned in the offsetParent's * coordinate space, which the pop rule's offsets are in. */ const measurePoint = (element) => { let { left, top } = element.getBoundingClientRect(); let node = element; while ((node = (node.parentElement || node.parentNode?.host))) { const { scrollLeft, scrollTop } = node; if (scrollLeft || scrollTop) { const [x, y] = measureScale(node); left += scrollLeft * x; top += scrollTop * y; } if (getComputedStyle(node).position === "fixed") break; } const [x, y] = measureScale(element.offsetParent); return [left / x, top / y]; }; /** * Measurement functionality has to be within a separate component * to leverage snapshot lifecycle. */ class PopChildMeasure extends React.Component { getSnapshotBeforeUpdate(prevProps) { const element = this.props.getElement(); if (isHTMLElement(element) && prevProps.isPresent && !this.props.isPresent && this.props.pop !== false) { const parent = element.offsetParent; const parentWidth = isHTMLElement(parent) ? parent.offsetWidth || 0 : 0; const parentHeight = isHTMLElement(parent) ? parent.offsetHeight || 0 : 0; const computedStyle = getComputedStyle(element); const size = this.props.sizeRef.current; size.height = parseFloat(computedStyle.height); size.width = parseFloat(computedStyle.width); size.top = element.offsetTop; size.left = element.offsetLeft; size.right = parentWidth - size.width - size.left; size.bottom = parentHeight - size.height - size.top; size.direction = computedStyle.direction; size.point = measurePoint(element); } return null; } /** * Required with getSnapshotBeforeUpdate to stop React complaining. */ componentDidUpdate() { } render() { return this.props.children; } } function PopChild({ children, isPresent, anchorX, anchorY, root, pop, popNode }) { const id = useId(); const ref = useRef(null); const size = useRef({ width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0, direction: "ltr", }); const { nonce } = useContext(MotionConfigContext); /** * In React 19, refs are passed via props.ref instead of element.ref. * We check props.ref first (React 19) and fall back to element.ref (React 18). * * The composed ref is only used when popping the child out (pop !== false), * so we avoid reading children.props.ref otherwise. In React 18.3 that read * triggers a spurious "`ref` is not a prop" warning getter (#3745). */ const childRef = pop !== false ? (children.props?.ref ?? children?.ref) : undefined; const composedRef = useComposedRefs(ref, childRef); /** * The child's element: the one our ref reached or, if the child can't * be given a ref (for instance a function component that doesn't * forward it), its outermost motion component's element (#2503). */ const getElement = () => ref.current || popNode?.current?.current; const insertPopRule = (sheet) => { const { width, height, top, left, right, bottom, direction } = size.current; const isRTL = direction === "rtl"; const x = anchorX === "left" ? (isRTL ? `right:${right}` : `left:${left}`) : (isRTL ? `left:${left}` : `right:${right}`); const y = anchorY === "bottom" ? `bottom:${bottom}` : `top:${top}`; sheet?.insertRule(`[data-motion-pop-id="${id}"]{position:absolute!important;width:${width}px!important;height:${height}px!important;${x}px!important;${y}px!important}`); }; /** * We create and inject a style block so we can apply this explicit * sizing in a non-destructive manner by just deleting the style block. * * We can't apply size via render as the measurement happens * in getSnapshotBeforeUpdate (post-render), likewise if we apply the * styles directly on the DOM node, we might be overwriting * styles set via the style prop. */ useInsertionEffect(() => { const { width, height } = size.current; const element = getElement(); if (isPresent || pop === false || !element || !width || !height) return; element.dataset.motionPopId = id; /** * Use the element's own document so this works when rendering * into another window or iframe (e.g. via window.open()). */ const doc = element.ownerDocument; const style = doc.createElement("style"); if (nonce) style.nonce = nonce; const parent = root ?? doc.head; parent.appendChild(style); insertPopRule(style.sheet); /** * Popping the element out of the layout can move its offsetParent, * which we've pinned it to. For instance, a bottom-anchored or * centred container shrinks around its remaining children (#2348). * So once the whole commit is in the DOM, measure how far the * element moved and offset it back. * * This runs on the microtask frameloop, so before paint even if * this commit happens within an animation frame. Every popped * element is measured in its first step, then corrected at the * start of the read step: before the projection system, which * schedules its layout measurement there from layout effects, so * layout animations see the corrected position. */ let dx = 0; let dy = 0; microtask.setup(() => { const [x, y] = measurePoint(element); dx = size.current.point[0] - x; dy = size.current.point[1] - y; }); microtask.read(() => { /** * The style has no sheet if it's been removed since, because * the element is present again. */ const { sheet } = style; if (!sheet || (!dx && !dy)) return; const s = size.current; s.left += dx; s.right -= dx; s.top += dy; s.bottom -= dy; sheet.deleteRule(0); insertPopRule(sheet); }); return () => { element.removeAttribute("data-motion-pop-id"); if (parent.contains(style)) { parent.removeChild(style); } }; }, [isPresent]); return (jsx(PopChildMeasure, { isPresent: isPresent, getElement: getElement, sizeRef: size, pop: pop, children: pop === false ? children : React.cloneElement(children, { ref: composedRef }) })); } export { PopChild }; //# sourceMappingURL=PopChild.mjs.map