UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

803 lines (770 loc) • 29.9 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var motionUtils = require('motion-utils'); var React = require('react'); var motionDom = require('motion-dom'); const LayoutGroupContext = React.createContext({}); /** * Creates a constant value over the lifecycle of a component. * * Even if `useMemo` is provided an empty array as its final argument, it doesn't offer * a guarantee that it won't re-run for performance reasons later on. By using `useConstant` * you can ensure that initialisers don't execute twice or more. */ function useConstant(init) { const ref = React.useRef(null); if (ref.current === null) { ref.current = init(); } return ref.current; } const isBrowser = typeof window !== "undefined"; const useIsomorphicLayoutEffect = isBrowser ? React.useLayoutEffect : React.useEffect; /** * @public */ const PresenceContext = /* @__PURE__ */ React.createContext(null); /** * @public */ const MotionConfigContext = React.createContext({ transformPagePoint: (p) => p, isStatic: false, reducedMotion: "never", }); const LazyContext = React.createContext({ strict: false }); const featureProps = { animation: [ "animate", "variants", "whileHover", "whileTap", "exit", "whileInView", "whileFocus", "whileDrag", ], exit: ["exit"], drag: ["drag", "dragControls"], focus: ["whileFocus"], hover: ["whileHover", "onHoverStart", "onHoverEnd"], tap: ["whileTap", "onTap", "onTapStart", "onTapCancel"], pan: ["onPan", "onPanStart", "onPanSessionStart", "onPanEnd"], inView: ["whileInView", "onViewportEnter", "onViewportLeave"], layout: ["layout", "layoutId"], }; let isInitialized = false; /** * Initialize feature definitions with isEnabled checks. * This must be called before any motion components are rendered. */ function initFeatureDefinitions() { if (isInitialized) return; const initialFeatureDefinitions = {}; for (const key in featureProps) { initialFeatureDefinitions[key] = { isEnabled: (props) => featureProps[key].some((name) => !!props[name]), }; } motionDom.setFeatureDefinitions(initialFeatureDefinitions); isInitialized = true; } /** * Get the current feature definitions, initializing if needed. */ function getInitializedFeatureDefinitions() { initFeatureDefinitions(); return motionDom.getFeatureDefinitions(); } function loadFeatures(features) { const featureDefinitions = getInitializedFeatureDefinitions(); for (const key in features) { featureDefinitions[key] = { ...featureDefinitions[key], ...features[key], }; } motionDom.setFeatureDefinitions(featureDefinitions); } /** * A list of all valid MotionProps. * * @privateRemarks * This doesn't throw if a `MotionProp` name is missing - it should. */ const validMotionProps = new Set([ "animate", "exit", "variants", "initial", "style", "values", "variants", "transition", "transformTemplate", "custom", "inherit", "onBeforeLayoutMeasure", "onAnimationStart", "onAnimationComplete", "onUpdate", "onDragStart", "onDrag", "onDragEnd", "onMeasureDragConstraints", "onDirectionLock", "onDragTransitionEnd", "_dragX", "_dragY", "onHoverStart", "onHoverEnd", "onViewportEnter", "onViewportLeave", "globalTapTarget", "propagate", "ignoreStrict", "viewport", ]); /** * Check whether a prop name is a valid `MotionProp` key. * * @param key - Name of the property to check * @returns `true` is key is a valid `MotionProp`. * * @public */ function isValidMotionProp(key) { return (key.startsWith("while") || (key.startsWith("drag") && key !== "draggable") || key.startsWith("layout") || key.startsWith("onTap") || key.startsWith("onPan") || key.startsWith("onLayout") || validMotionProps.has(key)); } let shouldForward = (key) => !isValidMotionProp(key); function loadExternalIsValidProp(isValidProp) { if (typeof isValidProp !== "function") return; // Explicitly filter our events shouldForward = (key) => key.startsWith("on") ? !isValidMotionProp(key) : isValidProp(key); } /** * Emotion and Styled Components both allow users to pass through arbitrary props to their components * to dynamically generate CSS. They both use the `@emotion/is-prop-valid` package to determine which * of these should be passed to the underlying DOM node. * * However, when styling a Motion component `styled(motion.div)`, both packages pass through *all* props * as it's seen as an arbitrary component rather than a DOM node. Motion only allows arbitrary props * passed through the `custom` prop so it doesn't *need* the payload or computational overhead of * `@emotion/is-prop-valid`, however to fix this problem we need to use it. * * By making it an optionalDependency we can offer this functionality only in the situations where it's * actually required. */ try { /** * We attempt to import this package but require won't be defined in esm environments, in that case * isPropValid will have to be provided via `MotionContext`. In a 6.0.0 this should probably be removed * in favour of explicit injection. * * String concatenation prevents bundlers like webpack (e.g. Storybook) * from statically resolving this optional dependency at build time. */ const emotionPkg = "@emotion/is-prop-" + "valid"; loadExternalIsValidProp(require(emotionPkg).default); } catch { // We don't need to actually do anything here - the fallback is the existing `isPropValid`. } function filterProps(props, isDom, forwardMotionProps) { const filteredProps = {}; for (const key in props) { /** * values is considered a valid prop by Emotion, so if it's present * this will be rendered out to the DOM unless explicitly filtered. * * We check the type as it could be used with the `feColorMatrix` * element, which we support. */ if (key === "values" && typeof props.values === "object") continue; if (motionDom.isMotionValue(props[key])) continue; if (shouldForward(key) || (forwardMotionProps === true && isValidMotionProp(key)) || (!isDom && !isValidMotionProp(key)) || // If trying to use native HTML drag events, forward drag listeners (props["draggable"] && key.startsWith("onDrag"))) { filteredProps[key] = props[key]; } } return filteredProps; } /** * We keep these listed separately as we use the lowercase tag names as part * of the runtime bundle to detect SVG components */ const lowercaseSVGElements = [ "animate", "circle", "defs", "desc", "ellipse", "g", "image", "line", "filter", "marker", "mask", "metadata", "path", "pattern", "polygon", "polyline", "rect", "stop", "switch", "symbol", "svg", "text", "tspan", "use", "view", ]; function isSVGComponent(Component) { if ( /** * If it's not a string, it's a custom React component. Currently we only support * HTML custom React components. */ typeof Component !== "string" || /** * If it contains a dash, the element is a custom HTML webcomponent. */ Component.includes("-")) { return false; } else if ( /** * If it's in our list of lowercase SVG tags, it's an SVG component */ lowercaseSVGElements.indexOf(Component) > -1 || /** * If it contains a capital letter, it's an SVG component */ /[A-Z]/u.test(Component)) { return true; } return false; } const MotionContext = /* @__PURE__ */ React.createContext({}); function getCurrentTreeVariants(props, context) { if (motionDom.isControllingVariants(props)) { const { initial, animate } = props; return { initial: initial === false || motionDom.isVariantLabel(initial) ? initial : undefined, animate: motionDom.isVariantLabel(animate) ? animate : undefined, }; } return props.inherit !== false ? context : {}; } function useCreateMotionContext(props) { const { initial, animate } = getCurrentTreeVariants(props, React.useContext(MotionContext)); return React.useMemo(() => ({ initial, animate }), [variantLabelsAsDependency(initial), variantLabelsAsDependency(animate)]); } function variantLabelsAsDependency(prop) { return Array.isArray(prop) ? prop.join(" ") : prop; } const createHtmlRenderState = () => ({ style: {}, transform: {}, transformOrigin: {}, vars: {}, }); function copyRawValuesOnly(target, source, props) { for (const key in source) { if (!motionDom.isMotionValue(source[key]) && !motionDom.isForcedMotionValue(key, props)) { target[key] = source[key]; } } } function useInitialMotionValues({ transformTemplate }, visualState) { return React.useMemo(() => { const state = createHtmlRenderState(); motionDom.buildHTMLStyles(state, visualState, transformTemplate); return Object.assign({}, state.vars, state.style); }, [visualState]); } function useStyle(props, visualState) { const styleProp = props.style || {}; const style = {}; /** * Copy non-Motion Values straight into style */ copyRawValuesOnly(style, styleProp, props); Object.assign(style, useInitialMotionValues(props, visualState)); return style; } function useHTMLProps(props, visualState) { // The `any` isn't ideal but it is the type of createElement props argument const htmlProps = {}; const style = useStyle(props, visualState); if (props.drag && props.dragListener !== false) { // Disable the ghost element when a user drags htmlProps.draggable = false; // Disable text selection style.userSelect = style.WebkitUserSelect = style.WebkitTouchCallout = "none"; // Disable scrolling on the draggable direction style.touchAction = props.drag === true ? "none" : `pan-${props.drag === "x" ? "y" : "x"}`; } if (props.tabIndex === undefined && (props.onTap || props.onTapStart || props.whileTap)) { htmlProps.tabIndex = 0; } htmlProps.style = style; return htmlProps; } const createSvgRenderState = () => ({ ...createHtmlRenderState(), attrs: {}, }); function useSVGProps(props, visualState, _isStatic, Component) { const visualProps = React.useMemo(() => { const state = createSvgRenderState(); motionDom.buildSVGAttrs(state, visualState, motionDom.isSVGTag(Component), props.transformTemplate, props.style); return { ...state.attrs, style: { ...state.style }, }; }, [visualState]); if (props.style) { const rawStyles = {}; copyRawValuesOnly(rawStyles, props.style, props); visualProps.style = { ...rawStyles, ...visualProps.style }; } return visualProps; } function useRender(Component, props, ref, { latestValues, }, isStatic, forwardMotionProps = false, isSVG) { const useVisualProps = (isSVG ?? isSVGComponent(Component)) ? useSVGProps : useHTMLProps; const visualProps = useVisualProps(props, latestValues, isStatic, Component); const filteredProps = filterProps(props, typeof Component === "string", forwardMotionProps); const elementProps = Component !== React.Fragment ? { ...filteredProps, ...visualProps, ref } : {}; /** * If component has been handed a motion value as its child, * memoise its initial value and render that. Subsequent updates * will be handled by the onChange handler */ const { children } = props; const renderedChildren = React.useMemo(() => (motionDom.isMotionValue(children) ? children.get() : children), [children]); return React.createElement(Component, { ...elementProps, children: renderedChildren, }); } function makeState({ scrapeMotionValuesFromProps, createRenderState, }, props, context, presenceContext) { const state = { latestValues: makeLatestValues(props, context, presenceContext, scrapeMotionValuesFromProps), renderState: createRenderState(), }; return state; } function makeLatestValues(props, context, presenceContext, scrapeMotionValues) { const values = {}; const motionValues = scrapeMotionValues(props, {}); for (const key in motionValues) { values[key] = motionDom.resolveMotionValue(motionValues[key]); } let { initial, animate } = props; const isControllingVariants = motionDom.isControllingVariants(props); const isVariantNode = motionDom.isVariantNode(props); if (context && isVariantNode && !isControllingVariants && props.inherit !== false) { if (initial === undefined) initial = context.initial; if (animate === undefined) animate = context.animate; } let isInitialAnimationBlocked = presenceContext ? presenceContext.initial === false : false; isInitialAnimationBlocked = isInitialAnimationBlocked || initial === false; const variantToSet = isInitialAnimationBlocked ? animate : initial; if (variantToSet && typeof variantToSet !== "boolean" && !motionDom.isAnimationControls(variantToSet)) { const list = Array.isArray(variantToSet) ? variantToSet : [variantToSet]; for (let i = 0; i < list.length; i++) { const resolved = motionDom.resolveVariantFromProps(props, list[i]); if (resolved) { const { transitionEnd, transition, ...target } = resolved; for (const key in target) { let valueTarget = target[key]; if (Array.isArray(valueTarget)) { /** * Take final keyframe if the initial animation is blocked because * we want to initialise at the end of that blocked animation. */ const index = isInitialAnimationBlocked ? valueTarget.length - 1 : 0; valueTarget = valueTarget[index]; } if (valueTarget !== null) { values[key] = valueTarget; } } for (const key in transitionEnd) { values[key] = transitionEnd[key]; } } } } return values; } const makeUseVisualState = (config) => (props, isStatic) => { const context = React.useContext(MotionContext); const presenceContext = React.useContext(PresenceContext); const make = () => makeState(config, props, context, presenceContext); return isStatic ? make() : useConstant(make); }; const useHTMLVisualState = /*@__PURE__*/ makeUseVisualState({ scrapeMotionValuesFromProps: motionDom.scrapeHTMLMotionValuesFromProps, createRenderState: createHtmlRenderState, }); const useSVGVisualState = /*@__PURE__*/ makeUseVisualState({ scrapeMotionValuesFromProps: motionDom.scrapeSVGMotionValuesFromProps, createRenderState: createSvgRenderState, }); const motionComponentSymbol = Symbol.for("motionComponentSymbol"); /** * Creates a ref function that, when called, hydrates the provided * external ref and VisualElement. */ function useMotionRef(visualState, visualElement, externalRef) { /** * Store externalRef in a ref to avoid including it in the useCallback * dependency array. Including externalRef in dependencies causes issues * with libraries like Radix UI that create new callback refs on each render * when using asChild - this would cause the callback to be recreated, * triggering element remounts and breaking AnimatePresence exit animations. */ const externalRefContainer = React.useRef(externalRef); React.useInsertionEffect(() => { externalRefContainer.current = externalRef; }); // Store cleanup function returned by callback refs (React 19 feature) const refCleanup = React.useRef(null); return React.useCallback((instance) => { if (instance) { visualState.onMount?.(instance); } if (visualElement) { instance ? visualElement.mount(instance) : visualElement.unmount(); } const ref = externalRefContainer.current; if (typeof ref === "function") { if (instance) { const cleanup = ref(instance); if (typeof cleanup === "function") { refCleanup.current = cleanup; } } else if (refCleanup.current) { refCleanup.current(); refCleanup.current = null; } else { ref(instance); } } else if (ref) { ref.current = instance; } }, [visualElement]); } /** * Internal, exported only for usage in Framer */ const SwitchLayoutGroupContext = React.createContext({}); function isRefObject(ref) { return (ref && typeof ref === "object" && Object.prototype.hasOwnProperty.call(ref, "current")); } function useVisualElement(Component, visualState, props, createVisualElement, ProjectionNodeConstructor, isSVG) { const { visualElement: parent } = React.useContext(MotionContext); const lazyContext = React.useContext(LazyContext); const presenceContext = React.useContext(PresenceContext); const motionConfig = React.useContext(MotionConfigContext); const reducedMotionConfig = motionConfig.reducedMotion; const skipAnimations = motionConfig.skipAnimations; const visualElementRef = React.useRef(null); /** * Track whether the component has been through React's commit phase. * Used to detect when LazyMotion features load after the component has mounted. */ const hasMountedOnce = React.useRef(false); /** * If we haven't preloaded a renderer, check to see if we have one lazy-loaded */ createVisualElement = createVisualElement || lazyContext.renderer; if (!visualElementRef.current && createVisualElement) { visualElementRef.current = createVisualElement(Component, { visualState, parent, props, presenceContext, blockInitialAnimation: presenceContext ? presenceContext.initial === false : false, reducedMotionConfig, skipAnimations, isSVG, }); /** * If the component has already mounted before features loaded (e.g. via * LazyMotion with async feature loading), we need to force the initial * animation to run. Otherwise state changes that occurred before features * loaded will be lost and the element will snap to its final state. */ if (hasMountedOnce.current && visualElementRef.current) { visualElementRef.current.manuallyAnimateOnMount = true; } } const visualElement = visualElementRef.current; /** * Load Motion gesture and animation features. These are rendered as renderless * components so each feature can optionally make use of React lifecycle methods. */ const initialLayoutGroupConfig = React.useContext(SwitchLayoutGroupContext); if (visualElement && !visualElement.projection && ProjectionNodeConstructor && (visualElement.type === "html" || visualElement.type === "svg")) { createProjectionNode(visualElementRef.current, props, ProjectionNodeConstructor, initialLayoutGroupConfig); } const isMounted = React.useRef(false); React.useInsertionEffect(() => { /** * Check the component has already mounted before calling * `update` unnecessarily. This ensures we skip the initial update. */ if (visualElement && isMounted.current) { visualElement.update(props, presenceContext); } }); /** * Cache this value as we want to know whether HandoffAppearAnimations * was present on initial render - it will be deleted after this. */ const optimisedAppearId = props[motionDom.optimizedAppearDataAttribute]; const wantsHandoff = React.useRef(Boolean(optimisedAppearId) && typeof window !== "undefined" && !window.MotionHandoffIsComplete?.(optimisedAppearId) && window.MotionHasOptimisedAnimation?.(optimisedAppearId)); useIsomorphicLayoutEffect(() => { /** * Track that this component has mounted. This is used to detect when * LazyMotion features load after the component has already committed. */ hasMountedOnce.current = true; if (!visualElement) return; isMounted.current = true; window.MotionIsMounted = true; visualElement.updateFeatures(); visualElement.scheduleRenderMicrotask(); /** * Ideally this function would always run in a useEffect. * * However, if we have optimised appear animations to handoff from, * it needs to happen synchronously to ensure there's no flash of * incorrect styles in the event of a hydration error. * * So if we detect a situtation where optimised appear animations * are running, we use useLayoutEffect to trigger animations. */ if (wantsHandoff.current && visualElement.animationState) { visualElement.animationState.animateChanges(); } }); React.useEffect(() => { if (!visualElement) return; if (!wantsHandoff.current && visualElement.animationState) { visualElement.animationState.animateChanges(); } if (wantsHandoff.current) { // This ensures all future calls to animateChanges() in this component will run in useEffect queueMicrotask(() => { window.MotionHandoffMarkAsComplete?.(optimisedAppearId); }); wantsHandoff.current = false; } /** * Now we've finished triggering animations for this element we * can wipe the enteringChildren set for the next render. */ visualElement.enteringChildren = undefined; }); return visualElement; } function createProjectionNode(visualElement, props, ProjectionNodeConstructor, initialPromotionConfig) { const { layoutId, layout, drag, dragConstraints, layoutScroll, layoutRoot, layoutAnchor, layoutCrossfade, } = props; visualElement.projection = new ProjectionNodeConstructor(visualElement.latestValues, props["data-framer-portal-id"] ? undefined : getClosestProjectingNode(visualElement.parent)); visualElement.projection.setOptions({ layoutId, layout, alwaysMeasureLayout: Boolean(drag) || (dragConstraints && isRefObject(dragConstraints)), visualElement, /** * TODO: Update options in an effect. This could be tricky as it'll be too late * to update by the time layout animations run. * We also need to fix this safeToRemove by linking it up to the one returned by usePresence, * ensuring it gets called if there's no potential layout animations. * */ animationType: typeof layout === "string" ? layout : "both", initialPromotionConfig, crossfade: layoutCrossfade, layoutScroll, layoutRoot, layoutAnchor, }); } function getClosestProjectingNode(visualElement) { if (!visualElement) return undefined; return visualElement.options.allowProjection !== false ? visualElement.projection : getClosestProjectingNode(visualElement.parent); } /** * Create a `motion` component. * * This function accepts a Component argument, which can be either a string (ie "div" * for `motion.div`), or an actual React component. * * Alongside this is a config option which provides a way of rendering the provided * component "offline", or outside the React render cycle. */ function createMotionComponent(Component, { forwardMotionProps = false, type } = {}, preloadedFeatures, createVisualElement) { preloadedFeatures && loadFeatures(preloadedFeatures); /** * Determine whether to use SVG or HTML rendering based on: * 1. Explicit `type` option (highest priority) * 2. Auto-detection via `isSVGComponent` */ const isSVG = type ? type === "svg" : isSVGComponent(Component); const useVisualState = isSVG ? useSVGVisualState : useHTMLVisualState; function MotionDOMComponent(props, externalRef) { /** * If we need to measure the element we load this functionality in a * separate class component in order to gain access to getSnapshotBeforeUpdate. */ let MeasureLayout; const configAndProps = { ...React.useContext(MotionConfigContext), ...props, layoutId: useLayoutId(props), }; const { isStatic } = configAndProps; const context = useCreateMotionContext(props); const visualState = useVisualState(props, isStatic); if (!isStatic && typeof window !== "undefined") { useStrictMode(configAndProps, preloadedFeatures); const layoutProjection = getProjectionFunctionality(configAndProps); MeasureLayout = layoutProjection.MeasureLayout; /** * Create a VisualElement for this component. A VisualElement provides a common * interface to renderer-specific APIs (ie DOM/Three.js etc) as well as * providing a way of rendering to these APIs outside of the React render loop * for more performant animations and interactions */ context.visualElement = useVisualElement(Component, visualState, configAndProps, createVisualElement, layoutProjection.ProjectionNode, isSVG); } /** * The mount order and hierarchy is specific to ensure our element ref * is hydrated by the time features fire their effects. */ return (jsxRuntime.jsxs(MotionContext.Provider, { value: context, children: [MeasureLayout && context.visualElement ? (jsxRuntime.jsx(MeasureLayout, { visualElement: context.visualElement, ...configAndProps })) : null, useRender(Component, props, useMotionRef(visualState, context.visualElement, externalRef), visualState, isStatic, forwardMotionProps, isSVG)] })); } MotionDOMComponent.displayName = `motion.${typeof Component === "string" ? Component : `create(${Component.displayName ?? Component.name ?? ""})`}`; const ForwardRefMotionComponent = React.forwardRef(MotionDOMComponent); ForwardRefMotionComponent[motionComponentSymbol] = Component; return ForwardRefMotionComponent; } function useLayoutId({ layoutId }) { const layoutGroupId = React.useContext(LayoutGroupContext).id; return layoutGroupId && layoutId !== undefined ? layoutGroupId + "-" + layoutId : layoutId; } function useStrictMode(configAndProps, preloadedFeatures) { const isStrict = React.useContext(LazyContext).strict; /** * If we're in development mode, check to make sure we're not rendering a motion component * as a child of LazyMotion, as this will break the file-size benefits of using it. */ if (process.env.NODE_ENV !== "production" && preloadedFeatures && isStrict) { const strictMessage = "You have rendered a `motion` component within a `LazyMotion` component. This will break tree shaking. Import and render a `m` component instead."; configAndProps.ignoreStrict ? motionUtils.warning(false, strictMessage, "lazy-strict-mode") : motionUtils.invariant(false, strictMessage, "lazy-strict-mode"); } } function getProjectionFunctionality(props) { const featureDefinitions = getInitializedFeatureDefinitions(); const { drag, layout } = featureDefinitions; if (!drag && !layout) return {}; const combined = { ...drag, ...layout }; return { MeasureLayout: drag?.isEnabled(props) || layout?.isEnabled(props) ? combined.MeasureLayout : undefined, ProjectionNode: combined.ProjectionNode, }; } exports.LayoutGroupContext = LayoutGroupContext; exports.LazyContext = LazyContext; exports.MotionConfigContext = MotionConfigContext; exports.MotionContext = MotionContext; exports.PresenceContext = PresenceContext; exports.SwitchLayoutGroupContext = SwitchLayoutGroupContext; exports.createMotionComponent = createMotionComponent; exports.filterProps = filterProps; exports.isBrowser = isBrowser; exports.isRefObject = isRefObject; exports.isSVGComponent = isSVGComponent; exports.isValidMotionProp = isValidMotionProp; exports.loadExternalIsValidProp = loadExternalIsValidProp; exports.loadFeatures = loadFeatures; exports.makeUseVisualState = makeUseVisualState; exports.motionComponentSymbol = motionComponentSymbol; exports.useConstant = useConstant; exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect; //# sourceMappingURL=index-6W16WHlG.js.map