UNPKG

framer-motion

Version:

A simple and powerful React animation library

1,505 lines (1,427 loc) • 319 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var React = require('react'); var styleValueTypes = require('style-value-types'); var popmotion = require('popmotion'); var heyListen = require('hey-listen'); var sync = require('framesync'); var dom = require('@motionone/dom'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var React__namespace = /*#__PURE__*/_interopNamespace(React); var sync__default = /*#__PURE__*/_interopDefaultLegacy(sync); /** * @public */ const MotionConfigContext = React.createContext({ transformPagePoint: (p) => p, isStatic: false, reducedMotion: "never", }); const MotionContext = React.createContext({}); function useVisualElementContext() { return React.useContext(MotionContext).visualElement; } /** * @public */ const PresenceContext = React.createContext(null); const isBrowser = typeof document !== "undefined"; const useIsomorphicLayoutEffect = isBrowser ? React.useLayoutEffect : React.useEffect; const LazyContext = React.createContext({ strict: false }); function useVisualElement(Component, visualState, props, createVisualElement) { const parent = useVisualElementContext(); const lazyContext = React.useContext(LazyContext); const presenceContext = React.useContext(PresenceContext); const reducedMotionConfig = React.useContext(MotionConfigContext).reducedMotion; const visualElementRef = React.useRef(undefined); /** * 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, presenceId: presenceContext ? presenceContext.id : undefined, blockInitialAnimation: presenceContext ? presenceContext.initial === false : false, reducedMotionConfig, }); } const visualElement = visualElementRef.current; useIsomorphicLayoutEffect(() => { visualElement && visualElement.syncRender(); }); React.useEffect(() => { if (visualElement && visualElement.animationState) { visualElement.animationState.animateChanges(); } }); useIsomorphicLayoutEffect(() => () => visualElement && visualElement.notifyUnmount(), []); return visualElement; } function isRefObject(ref) { return (typeof ref === "object" && Object.prototype.hasOwnProperty.call(ref, "current")); } /** * Creates a ref function that, when called, hydrates the provided * external ref and VisualElement. */ function useMotionRef(visualState, visualElement, externalRef) { return React.useCallback((instance) => { instance && visualState.mount && visualState.mount(instance); if (visualElement) { instance ? visualElement.mount(instance) : visualElement.unmount(); } if (externalRef) { if (typeof externalRef === "function") { externalRef(instance); } else if (isRefObject(externalRef)) { externalRef.current = instance; } } }, /** * Only pass a new ref callback to React if we've received a visual element * factory. Otherwise we'll be mounting/remounting every time externalRef * or other dependencies change. */ [visualElement]); } /** * Decides if the supplied variable is variant label */ function isVariantLabel(v) { return typeof v === "string" || Array.isArray(v); } function isAnimationControls(v) { return typeof v === "object" && typeof v.start === "function"; } const variantProps$1 = [ "initial", "animate", "exit", "whileHover", "whileDrag", "whileTap", "whileFocus", "whileInView", ]; function isControllingVariants(props) { return (isAnimationControls(props.animate) || variantProps$1.some((name) => isVariantLabel(props[name]))); } function isVariantNode(props) { return Boolean(isControllingVariants(props) || props.variants); } function getCurrentTreeVariants(props, context) { if (isControllingVariants(props)) { const { initial, animate } = props; return { initial: initial === false || isVariantLabel(initial) ? initial : undefined, animate: 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 createDefinition = (propNames) => ({ isEnabled: (props) => propNames.some((name) => !!props[name]), }); const featureDefinitions = { measureLayout: createDefinition(["layout", "layoutId", "drag"]), animation: createDefinition([ "animate", "exit", "variants", "whileHover", "whileTap", "whileFocus", "whileDrag", "whileInView", ]), exit: createDefinition(["exit"]), drag: createDefinition(["drag", "dragControls"]), focus: createDefinition(["whileFocus"]), hover: createDefinition(["whileHover", "onHoverStart", "onHoverEnd"]), tap: createDefinition(["whileTap", "onTap", "onTapStart", "onTapCancel"]), pan: createDefinition([ "onPan", "onPanStart", "onPanSessionStart", "onPanEnd", ]), inView: createDefinition([ "whileInView", "onViewportEnter", "onViewportLeave", ]), }; function loadFeatures(features) { for (const key in features) { if (key === "projectionNodeConstructor") { featureDefinitions.projectionNodeConstructor = features[key]; } else { featureDefinitions[key].Component = features[key]; } } } /** * 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; } /** * This should only ever be modified on the client otherwise it'll * persist through server requests. If we need instanced states we * could lazy-init via root. */ const globalProjectionState = { /** * Global flag as to whether the tree has animated since the last time * we resized the window */ hasAnimatedSinceResize: true, /** * We set this to true once, on the first update. Any nodes added to the tree beyond that * update will be given a `data-projection-id` attribute. */ hasEverUpdated: false, }; let id$1 = 1; function useProjectionId() { return useConstant(() => { if (globalProjectionState.hasEverUpdated) { return id$1++; } }); } const LayoutGroupContext = React.createContext({}); class VisualElementHandler extends React__default["default"].Component { /** * Update visual element props as soon as we know this update is going to be commited. */ getSnapshotBeforeUpdate() { const { visualElement, props } = this.props; if (visualElement) visualElement.setProps(props); return null; } componentDidUpdate() { } render() { return this.props.children; } } /** * Internal, exported only for usage in Framer */ const SwitchLayoutGroupContext = React.createContext({}); const motionComponentSymbol = Symbol.for("motionComponentSymbol"); /** * 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({ preloadedFeatures, createVisualElement, projectionNodeConstructor, useRender, useVisualState, Component, }) { preloadedFeatures && loadFeatures(preloadedFeatures); function MotionComponent(props, externalRef) { const configAndProps = { ...React.useContext(MotionConfigContext), ...props, layoutId: useLayoutId(props), }; const { isStatic } = configAndProps; let features = null; const context = useCreateMotionContext(props); /** * Create a unique projection ID for this component. If a new component is added * during a layout animation we'll use this to query the DOM and hydrate its ref early, allowing * us to measure it as soon as any layout effect flushes pending layout animations. * * Performance note: It'd be better not to have to search the DOM for these elements. * For newly-entering components it could be enough to only correct treeScale, in which * case we could mount in a scale-correction mode. This wouldn't be enough for * shared element transitions however. Perhaps for those we could revert to a root node * that gets forceRendered and layout animations are triggered on its layout effect. */ const projectionId = isStatic ? undefined : useProjectionId(); /** * */ const visualState = useVisualState(props, isStatic); if (!isStatic && isBrowser) { /** * 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); /** * Load Motion gesture and animation features. These are rendered as renderless * components so each feature can optionally make use of React lifecycle methods. */ const lazyStrictMode = React.useContext(LazyContext).strict; const initialLayoutGroupConfig = React.useContext(SwitchLayoutGroupContext); if (context.visualElement) { features = context.visualElement.loadFeatures(props, lazyStrictMode, preloadedFeatures, projectionId, projectionNodeConstructor || featureDefinitions.projectionNodeConstructor, initialLayoutGroupConfig); } } /** * The mount order and hierarchy is specific to ensure our element ref * is hydrated by the time features fire their effects. */ return (React__namespace.createElement(VisualElementHandler, { visualElement: context.visualElement, props: configAndProps }, features, React__namespace.createElement(MotionContext.Provider, { value: context }, useRender(Component, props, projectionId, useMotionRef(visualState, context.visualElement, externalRef), visualState, isStatic, context.visualElement)))); } const ForwardRefComponent = React.forwardRef(MotionComponent); ForwardRefComponent[motionComponentSymbol] = Component; return ForwardRefComponent; } function useLayoutId({ layoutId }) { const layoutGroupId = React.useContext(LayoutGroupContext).id; return layoutGroupId && layoutId !== undefined ? layoutGroupId + "-" + layoutId : layoutId; } /** * Convert any React component into a `motion` component. The provided component * **must** use `React.forwardRef` to the underlying DOM component you want to animate. * * ```jsx * const Component = React.forwardRef((props, ref) => { * return <div ref={ref} /> * }) * * const MotionComponent = motion(Component) * ``` * * @public */ function createMotionProxy(createConfig) { function custom(Component, customMotionComponentConfig = {}) { return createMotionComponent(createConfig(Component, customMotionComponentConfig)); } if (typeof Proxy === "undefined") { return custom; } /** * A cache of generated `motion` components, e.g `motion.div`, `motion.input` etc. * Rather than generating them anew every render. */ const componentCache = new Map(); return new Proxy(custom, { /** * Called when `motion` is referenced with a prop: `motion.div`, `motion.input` etc. * The prop name is passed through as `key` and we can use that to generate a `motion` * DOM component with that name. */ get: (_target, key) => { /** * If this element doesn't exist in the component cache, create it and cache. */ if (!componentCache.has(key)) { componentCache.set(key, custom(key)); } return componentCache.get(key); }, }); } /** * We keep these listed seperately 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", "svg", "switch", "symbol", "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]/.test(Component)) { return true; } return false; } const scaleCorrectors = {}; function addScaleCorrector(correctors) { Object.assign(scaleCorrectors, correctors); } /** * Generate a list of every possible transform key. */ const transformPropOrder = [ "transformPerspective", "x", "y", "z", "translateX", "translateY", "translateZ", "scale", "scaleX", "scaleY", "rotate", "rotateX", "rotateY", "rotateZ", "skew", "skewX", "skewY", ]; /** * A quick lookup for transform props. */ const transformProps = new Set(transformPropOrder); function isForcedMotionValue(key, { layout, layoutId }) { return (transformProps.has(key) || key.startsWith("origin") || ((layout || layoutId !== undefined) && (!!scaleCorrectors[key] || key === "opacity"))); } const isMotionValue = (value) => value === undefined ? false : !!value.getVelocity; const translateAlias = { x: "translateX", y: "translateY", z: "translateZ", transformPerspective: "perspective", }; /** * A function to use with Array.sort to sort transform keys by their default order. */ const sortTransformProps = (a, b) => transformPropOrder.indexOf(a) - transformPropOrder.indexOf(b); /** * Build a CSS transform style from individual x/y/scale etc properties. * * This outputs with a default order of transforms/scales/rotations, this can be customised by * providing a transformTemplate function. */ function buildTransform({ transform, transformKeys }, { enableHardwareAcceleration = true, allowTransformNone = true, }, transformIsDefault, transformTemplate) { // The transform string we're going to build into. let transformString = ""; // Transform keys into their default order - this will determine the output order. transformKeys.sort(sortTransformProps); // Loop over each transform and build them into transformString for (const key of transformKeys) { transformString += `${translateAlias[key] || key}(${transform[key]}) `; } if (enableHardwareAcceleration && !transform.z) { transformString += "translateZ(0)"; } transformString = transformString.trim(); // If we have a custom `transform` template, pass our transform values and // generated transformString to that before returning if (transformTemplate) { transformString = transformTemplate(transform, transformIsDefault ? "" : transformString); } else if (allowTransformNone && transformIsDefault) { transformString = "none"; } return transformString; } /** * Returns true if the provided key is a CSS variable */ function isCSSVariable$1(key) { return key.startsWith("--"); } /** * Provided a value and a ValueType, returns the value as that value type. */ const getValueAsType = (value, type) => { return type && typeof value === "number" ? type.transform(value) : value; }; const int = { ...styleValueTypes.number, transform: Math.round, }; const numberValueTypes = { // Border props borderWidth: styleValueTypes.px, borderTopWidth: styleValueTypes.px, borderRightWidth: styleValueTypes.px, borderBottomWidth: styleValueTypes.px, borderLeftWidth: styleValueTypes.px, borderRadius: styleValueTypes.px, radius: styleValueTypes.px, borderTopLeftRadius: styleValueTypes.px, borderTopRightRadius: styleValueTypes.px, borderBottomRightRadius: styleValueTypes.px, borderBottomLeftRadius: styleValueTypes.px, // Positioning props width: styleValueTypes.px, maxWidth: styleValueTypes.px, height: styleValueTypes.px, maxHeight: styleValueTypes.px, size: styleValueTypes.px, top: styleValueTypes.px, right: styleValueTypes.px, bottom: styleValueTypes.px, left: styleValueTypes.px, // Spacing props padding: styleValueTypes.px, paddingTop: styleValueTypes.px, paddingRight: styleValueTypes.px, paddingBottom: styleValueTypes.px, paddingLeft: styleValueTypes.px, margin: styleValueTypes.px, marginTop: styleValueTypes.px, marginRight: styleValueTypes.px, marginBottom: styleValueTypes.px, marginLeft: styleValueTypes.px, // Transform props rotate: styleValueTypes.degrees, rotateX: styleValueTypes.degrees, rotateY: styleValueTypes.degrees, rotateZ: styleValueTypes.degrees, scale: styleValueTypes.scale, scaleX: styleValueTypes.scale, scaleY: styleValueTypes.scale, scaleZ: styleValueTypes.scale, skew: styleValueTypes.degrees, skewX: styleValueTypes.degrees, skewY: styleValueTypes.degrees, distance: styleValueTypes.px, translateX: styleValueTypes.px, translateY: styleValueTypes.px, translateZ: styleValueTypes.px, x: styleValueTypes.px, y: styleValueTypes.px, z: styleValueTypes.px, perspective: styleValueTypes.px, transformPerspective: styleValueTypes.px, opacity: styleValueTypes.alpha, originX: styleValueTypes.progressPercentage, originY: styleValueTypes.progressPercentage, originZ: styleValueTypes.px, // Misc zIndex: int, // SVG fillOpacity: styleValueTypes.alpha, strokeOpacity: styleValueTypes.alpha, numOctaves: int, }; function buildHTMLStyles(state, latestValues, options, transformTemplate) { const { style, vars, transform, transformKeys, transformOrigin } = state; transformKeys.length = 0; // Track whether we encounter any transform or transformOrigin values. let hasTransform = false; let hasTransformOrigin = false; // Does the calculated transform essentially equal "none"? let transformIsNone = true; /** * Loop over all our latest animated values and decide whether to handle them * as a style or CSS variable. * * Transforms and transform origins are kept seperately for further processing. */ for (const key in latestValues) { const value = latestValues[key]; /** * If this is a CSS variable we don't do any further processing. */ if (isCSSVariable$1(key)) { vars[key] = value; continue; } // Convert the value to its default value type, ie 0 -> "0px" const valueType = numberValueTypes[key]; const valueAsType = getValueAsType(value, valueType); if (transformProps.has(key)) { // If this is a transform, flag to enable further transform processing hasTransform = true; transform[key] = valueAsType; transformKeys.push(key); // If we already know we have a non-default transform, early return if (!transformIsNone) continue; // Otherwise check to see if this is a default transform if (value !== (valueType.default || 0)) transformIsNone = false; } else if (key.startsWith("origin")) { // If this is a transform origin, flag and enable further transform-origin processing hasTransformOrigin = true; transformOrigin[key] = valueAsType; } else { style[key] = valueAsType; } } if (hasTransform || transformTemplate) { style.transform = buildTransform(state, options, transformIsNone, transformTemplate); } else if (!latestValues.transform && style.transform) { /** * If we have previously created a transform but currently don't have any, * reset transform style to none. */ style.transform = "none"; } /** * Build a transformOrigin style. Uses the same defaults as the browser for * undefined origins. */ if (hasTransformOrigin) { const { originX = "50%", originY = "50%", originZ = 0, } = transformOrigin; style.transformOrigin = `${originX} ${originY} ${originZ}`; } } const createHtmlRenderState = () => ({ style: {}, transform: {}, transformKeys: [], transformOrigin: {}, vars: {}, }); function copyRawValuesOnly(target, source, props) { for (const key in source) { if (!isMotionValue(source[key]) && !isForcedMotionValue(key, props)) { target[key] = source[key]; } } } function useInitialMotionValues({ transformTemplate }, visualState, isStatic) { return React.useMemo(() => { const state = createHtmlRenderState(); buildHTMLStyles(state, visualState, { enableHardwareAcceleration: !isStatic }, transformTemplate); return Object.assign({}, state.vars, state.style); }, [visualState]); } function useStyle(props, visualState, isStatic) { const styleProp = props.style || {}; const style = {}; /** * Copy non-Motion Values straight into style */ copyRawValuesOnly(style, styleProp, props); Object.assign(style, useInitialMotionValues(props, visualState, isStatic)); return props.transformValues ? props.transformValues(style) : style; } function useHTMLProps(props, visualState, isStatic) { // The `any` isn't ideal but it is the type of createElement props argument const htmlProps = {}; const style = useStyle(props, visualState, isStatic); 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"}`; } htmlProps.style = style; return htmlProps; } const animationProps = [ "animate", "exit", "variants", "whileHover", "whileTap", "whileFocus", "whileDrag", "whileInView", ]; const tapProps = ["whileTap", "onTap", "onTapStart", "onTapCancel"]; const panProps = ["onPan", "onPanStart", "onPanSessionStart", "onPanEnd"]; const inViewProps = [ "whileInView", "onViewportEnter", "onViewportLeave", "viewport", ]; /** * A list of all valid MotionProps. * * @privateRemarks * This doesn't throw if a `MotionProp` name is missing - it should. */ const validMotionProps = new Set([ "initial", "style", "variants", "transition", "transformTemplate", "transformValues", "custom", "inherit", "layout", "layoutId", "layoutDependency", "onLayoutAnimationStart", "onLayoutAnimationComplete", "onLayoutMeasure", "onBeforeLayoutMeasure", "onAnimationStart", "onAnimationComplete", "onUpdate", "onDragStart", "onDrag", "onDragEnd", "onMeasureDragConstraints", "onDirectionLock", "onDragTransitionEnd", "drag", "dragControls", "dragListener", "dragConstraints", "dragDirectionLock", "dragSnapToOrigin", "_dragX", "_dragY", "dragElastic", "dragMomentum", "dragPropagation", "dragTransition", "onHoverStart", "onHoverEnd", "layoutScroll", ...inViewProps, ...tapProps, ...animationProps, ...panProps, ]); /** * 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 validMotionProps.has(key); } let shouldForward = (key) => !isValidMotionProp(key); function loadExternalIsValidProp(isValidProp) { if (!isValidProp) 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. */ loadExternalIsValidProp(require("@emotion/is-prop-valid").default); } catch (_a) { // 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) { 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; } function calcOrigin$1(origin, offset, size) { return typeof origin === "string" ? origin : styleValueTypes.px.transform(offset + size * origin); } /** * The SVG transform origin defaults are different to CSS and is less intuitive, * so we use the measured dimensions of the SVG to reconcile these. */ function calcSVGTransformOrigin(dimensions, originX, originY) { const pxOriginX = calcOrigin$1(originX, dimensions.x, dimensions.width); const pxOriginY = calcOrigin$1(originY, dimensions.y, dimensions.height); return `${pxOriginX} ${pxOriginY}`; } const dashKeys = { offset: "stroke-dashoffset", array: "stroke-dasharray", }; const camelKeys = { offset: "strokeDashoffset", array: "strokeDasharray", }; /** * Build SVG path properties. Uses the path's measured length to convert * our custom pathLength, pathSpacing and pathOffset into stroke-dashoffset * and stroke-dasharray attributes. * * This function is mutative to reduce per-frame GC. */ function buildSVGPath(attrs, length, spacing = 1, offset = 0, useDashCase = true) { // Normalise path length by setting SVG attribute pathLength to 1 attrs.pathLength = 1; // We use dash case when setting attributes directly to the DOM node and camel case // when defining props on a React component. const keys = useDashCase ? dashKeys : camelKeys; // Build the dash offset attrs[keys.offset] = styleValueTypes.px.transform(-offset); // Build the dash array const pathLength = styleValueTypes.px.transform(length); const pathSpacing = styleValueTypes.px.transform(spacing); attrs[keys.array] = `${pathLength} ${pathSpacing}`; } /** * Build SVG visual attrbutes, like cx and style.transform */ function buildSVGAttrs(state, { attrX, attrY, originX, originY, pathLength, pathSpacing = 1, pathOffset = 0, // This is object creation, which we try to avoid per-frame. ...latest }, options, transformTemplate) { buildHTMLStyles(state, latest, options, transformTemplate); state.attrs = state.style; state.style = {}; const { attrs, style, dimensions } = state; /** * However, we apply transforms as CSS transforms. So if we detect a transform we take it from attrs * and copy it into style. */ if (attrs.transform) { if (dimensions) style.transform = attrs.transform; delete attrs.transform; } // Parse transformOrigin if (dimensions && (originX !== undefined || originY !== undefined || style.transform)) { style.transformOrigin = calcSVGTransformOrigin(dimensions, originX !== undefined ? originX : 0.5, originY !== undefined ? originY : 0.5); } // Treat x/y not as shortcuts but as actual attributes if (attrX !== undefined) attrs.x = attrX; if (attrY !== undefined) attrs.y = attrY; // Build SVG path if one has been defined if (pathLength !== undefined) { buildSVGPath(attrs, pathLength, pathSpacing, pathOffset, false); } } const createSvgRenderState = () => ({ ...createHtmlRenderState(), attrs: {}, }); function useSVGProps(props, visualState) { const visualProps = React.useMemo(() => { const state = createSvgRenderState(); buildSVGAttrs(state, visualState, { enableHardwareAcceleration: false }, props.transformTemplate); 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 createUseRender(forwardMotionProps = false) { const useRender = (Component, props, projectionId, ref, { latestValues }, isStatic) => { const useVisualProps = isSVGComponent(Component) ? useSVGProps : useHTMLProps; const visualProps = useVisualProps(props, latestValues, isStatic); const filteredProps = filterProps(props, typeof Component === "string", forwardMotionProps); const elementProps = { ...filteredProps, ...visualProps, ref, }; if (projectionId) { elementProps["data-projection-id"] = projectionId; } return React.createElement(Component, elementProps); }; return useRender; } /** * Convert camelCase to dash-case properties. */ const camelToDash = (str) => str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); function renderHTML(element, { style, vars }, styleProp, projection) { Object.assign(element.style, style, projection && projection.getProjectionStyles(styleProp)); // Loop over any CSS variables and assign those. for (const key in vars) { element.style.setProperty(key, vars[key]); } } /** * A set of attribute names that are always read/written as camel case. */ const camelCaseAttributes = new Set([ "baseFrequency", "diffuseConstant", "kernelMatrix", "kernelUnitLength", "keySplines", "keyTimes", "limitingConeAngle", "markerHeight", "markerWidth", "numOctaves", "targetX", "targetY", "surfaceScale", "specularConstant", "specularExponent", "stdDeviation", "tableValues", "viewBox", "gradientTransform", "pathLength", ]); function renderSVG(element, renderState, _styleProp, projection) { renderHTML(element, renderState, undefined, projection); for (const key in renderState.attrs) { element.setAttribute(!camelCaseAttributes.has(key) ? camelToDash(key) : key, renderState.attrs[key]); } } function scrapeMotionValuesFromProps$1(props) { const { style } = props; const newValues = {}; for (const key in style) { if (isMotionValue(style[key]) || isForcedMotionValue(key, props)) { newValues[key] = style[key]; } } return newValues; } function scrapeMotionValuesFromProps(props) { const newValues = scrapeMotionValuesFromProps$1(props); for (const key in props) { if (isMotionValue(props[key])) { const targetKey = key === "x" || key === "y" ? "attr" + key.toUpperCase() : key; newValues[targetKey] = props[key]; } } return newValues; } function resolveVariantFromProps(props, definition, custom, currentValues = {}, currentVelocity = {}) { /** * If the variant definition is a function, resolve. */ if (typeof definition === "function") { definition = definition(custom !== undefined ? custom : props.custom, currentValues, currentVelocity); } /** * If the variant definition is a variant label, or * the function returned a variant label, resolve. */ if (typeof definition === "string") { definition = props.variants && props.variants[definition]; } /** * At this point we've resolved both functions and variant labels, * but the resolved variant label might itself have been a function. * If so, resolve. This can only have returned a valid target object. */ if (typeof definition === "function") { definition = definition(custom !== undefined ? custom : props.custom, currentValues, currentVelocity); } return definition; } const isKeyframesTarget = (v) => { return Array.isArray(v); }; const isCustomValue = (v) => { return Boolean(v && typeof v === "object" && v.mix && v.toValue); }; const resolveFinalValueInKeyframes = (v) => { // TODO maybe throw if v.length - 1 is placeholder token? return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v; }; /** * If the provided value is a MotionValue, this returns the actual value, otherwise just the value itself * * TODO: Remove and move to library */ function resolveMotionValue(value) { const unwrappedValue = isMotionValue(value) ? value.get() : value; return isCustomValue(unwrappedValue) ? unwrappedValue.toValue() : unwrappedValue; } function makeState({ scrapeMotionValuesFromProps, createRenderState, onMount, }, props, context, presenceContext) { const state = { latestValues: makeLatestValues(props, context, presenceContext, scrapeMotionValuesFromProps), renderState: createRenderState(), }; if (onMount) { state.mount = (instance) => onMount(props, instance, state); } return state; } 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); }; function makeLatestValues(props, context, presenceContext, scrapeMotionValues) { const values = {}; const motionValues = scrapeMotionValues(props); for (const key in motionValues) { values[key] = resolveMotionValue(motionValues[key]); } let { initial, animate } = props; const isControllingVariants$1 = isControllingVariants(props); const isVariantNode$1 = isVariantNode(props); if (context && isVariantNode$1 && !isControllingVariants$1 && 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" && !isAnimationControls(variantToSet)) { const list = Array.isArray(variantToSet) ? variantToSet : [variantToSet]; list.forEach((definition) => { const resolved = resolveVariantFromProps(props, definition); if (!resolved) return; 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 svgMotionConfig = { useVisualState: makeUseVisualState({ scrapeMotionValuesFromProps: scrapeMotionValuesFromProps, createRenderState: createSvgRenderState, onMount: (props, instance, { renderState, latestValues }) => { try { renderState.dimensions = typeof instance.getBBox === "function" ? instance.getBBox() : instance.getBoundingClientRect(); } catch (e) { // Most likely trying to measure an unrendered element under Firefox renderState.dimensions = { x: 0, y: 0, width: 0, height: 0, }; } buildSVGAttrs(renderState, latestValues, { enableHardwareAcceleration: false }, props.transformTemplate); renderSVG(instance, renderState); }, }), }; const htmlMotionConfig = { useVisualState: makeUseVisualState({ scrapeMotionValuesFromProps: scrapeMotionValuesFromProps$1, createRenderState: createHtmlRenderState, }), }; function createDomMotionConfig(Component, { forwardMotionProps = false }, preloadedFeatures, createVisualElement, projectionNodeConstructor) { const baseConfig = isSVGComponent(Component) ? svgMotionConfig : htmlMotionConfig; return { ...baseConfig, preloadedFeatures, useRender: createUseRender(forwardMotionProps), createVisualElement, projectionNodeConstructor, Component, }; } exports.AnimationType = void 0; (function (AnimationType) { AnimationType["Animate"] = "animate"; AnimationType["Hover"] = "whileHover"; AnimationType["Tap"] = "whileTap"; AnimationType["Drag"] = "whileDrag"; AnimationType["Focus"] = "whileFocus"; AnimationType["InView"] = "whileInView"; AnimationType["Exit"] = "exit"; })(exports.AnimationType || (exports.AnimationType = {})); function addDomEvent(target, eventName, handler, options = { passive: true }) { target.addEventListener(eventName, handler, options); return () => target.removeEventListener(eventName, handler); } /** * Attaches an event listener directly to the provided DOM element. * * Bypassing React's event system can be desirable, for instance when attaching non-passive * event handlers. * * ```jsx * const ref = useRef(null) * * useDomEvent(ref, 'wheel', onWheel, { passive: false }) * * return <div ref={ref} /> * ``` * * @param ref - React.RefObject that's been provided to the element you want to bind the listener to. * @param eventName - Name of the event you want listen for. * @param handler - Function to fire when receiving the event. * @param options - Options to pass to `Event.addEventListener`. * * @public */ function useDomEvent(ref, eventName, handler, options) { React.useEffect(() => { const element = ref.current; if (handler && element) { return addDomEvent(element, eventName, handler, options); } }, [ref, eventName, handler, options]); } /** * * @param props * @param ref * @internal */ function useFocusGesture({ whileFocus, visualElement }) { const { animationState } = visualElement; const onFocus = () => { animationState && animationState.setActive(exports.AnimationType.Focus, true); }; const onBlur = () => { animationState && animationState.setActive(exports.AnimationType.Focus, false); }; useDomEvent(visualElement, "focus", whileFocus ? onFocus : undefined); useDomEvent(visualElement, "blur", whileFocus ? onBlur : undefined); } function isMouseEvent(event) { // PointerEvent inherits from MouseEvent so we can't use a straight instanceof check. if (typeof PointerEvent !== "undefined" && event instanceof PointerEvent) { return !!(event.pointerType === "mouse"); } return event instanceof MouseEvent; } function isTouchEvent(event) { const hasTouches = !!event.touches; return hasTouches; } /** * Filters out events not attached to the primary pointer (currently left mouse button) * @param eventHandler */ function filterPrimaryPointer(eventHandler) { return (event) => { const isMouseEvent = event instanceof MouseEvent; const isPrimaryPointer = !isMouseEvent || (isMouseEvent && event.button === 0); if (isPrimaryPointer) { eventHandler(event); } }; } const defaultPagePoint = { pageX: 0, pageY: 0 }; function pointFromTouch(e, pointType = "page") { const primaryTouch = e.touches[0] || e.changedTouches[0]; const point = primaryTouch || defaultPagePoint; return { x: point[pointType + "X"], y: point[pointType + "Y"], }; } function pointFromMouse(point, pointType = "page") { return { x: point[pointType + "X"], y: point[pointType + "Y"], }; } function extractEventInfo(event, pointType = "page") { return { point: isTouchEvent(event) ? pointFromTouch(event, pointType) : pointFromMouse(event, pointType), }; } const wrapHandler = (handler, shouldFilterPrimaryPointer = false) => { const listener = (event) => handler(event, extractEventInfo(event)); return shouldFilterPrimaryPointer ? filterPrimaryPointer(listener) : listener; }; // We check for event support via functions in case they've been mocked by a testing suite. const supportsPointerEvents = () => isBrowser && window.onpointerdown === null; const supportsTouchEvents = () => isBrowser && window.ontouchstart === null; const supportsMouseEvents = () => isBrowser && window.onmousedown === null; const mouseEventNames = { pointerdown: "mousedown", pointermove: "mousemove", pointerup: "mouseup", pointercancel: "mousecancel", pointerover: "mouseover", pointerout: "mouseout", pointerenter: "mouseenter", pointerleave: "mouseleave", }; const touchEventNames = { pointerdown: "touchstart", pointermove: "touchmove", pointerup: "touchend", pointercancel: "touchcancel", }; function getPointerEventName(name) { if (supportsPointerEvents()) { return name; } else if (supportsTouchEvents()) { return touchEventNames[name]; } else if (supportsMouseEvents()) { return mouseEventNames[name]; } return name; } function addPointerEvent(target, eventName, handler, options) { return addDomEvent(target, getPointerEventName(eventName), wrapHandler(handler, eventName === "pointerdown"), options); } function usePointerEvent(ref, eventName, handler, options) { return useDomEvent(ref, getPointerEventName(eventName), handler && wrapHandler(handler, eventName === "pointerdown"), options); } function createLock(name) { let lock = null; return () => { const openLock = () => { lock = null; }; if (lock === null) { lock = name; return openLock; } return false; }; } const globalHorizontalLock = createLock("dragHorizontal"); const globalVerticalLock = createLock("dragVertical"); function getGlobalLock(drag) { let lock = false; if (drag === "y") { lock = globalVerticalLock(); } else if (drag === "x") { lock = globalHorizontalLock(); } else { const openHorizontal = globalHorizontalLock(); const openVertical = globalVerticalLock(); if (openHorizontal && openVertical) { lock = () => { openHorizontal(); openVertical(); }; } else { // Release the locks because we don't use them if (openHorizontal) openHorizontal(); if (openVertical) openVertical(); } } return lock; } function isDragActive() { // Check the gesture lock - if we get it, it means no drag gesture is active // and we can safely fire the tap gesture. const openGestureLock = getGlobalLock(true); if (!openGestureLock) return true; openGestureLock(); return false; } function createHoverEvent(visualElement, isActive, callback) { return (event, info) => { if (!isMouseEvent(event) || isDragActive()) return; /** * Ensure we trigger animations before firing event callback */ if (visualElement.animationState) { visualElement.animationState.setActive(exports.AnimationType.Hover, isActive); } callback && callback(event, info); }; } function useHoverGesture({ onHoverStart, onHoverEnd, whileHover, visualElement, }) { usePointerEvent(visualElement, "pointerenter", onHoverStart || whileHover ? createHoverEvent(visualElement, true, onHoverStart) : undefined, { passive: !onHoverStart }); usePointerEvent(visualElement, "pointerleave", onHoverEnd || whileHover ? createHoverEvent(visualElement, false, onHoverEnd) : undefined, { passive: !onHoverEnd }); } /** * Recursively traverse up the tree to check whether the provided child node * is the parent or a descendant of it. * * @param parent - Element to find * @param child - Element to test against parent */ const isNodeOrChild = (parent, child) => { if (!child) { return false; } else if (parent === child) { return true; } else { return isNodeOrChild(parent, child.parentElement); } }; function useUnmountEffect(callback) { return React.useEffect(() => () => callback(), []); } /** * @param handlers - * @internal */ function useTapGesture({ onTap, onTapStart, onTapCancel, whileTap, visualElement, }) { const hasPressListeners = onTap || onTapStart || onTapCancel || whileTap; const isPressing = React.useRef(false); const cancelPointerEndListeners = React.useRef(null); /** * Only set listener to pas