framer-motion
Version:
An open source and production-ready motion library for React on the web
104 lines (86 loc) • 3.38 kB
text/typescript
import { Target, TargetWithKeyframes } from "../../../types"
import { invariant } from "hey-listen"
import { VisualElement } from "../../types"
function isCSSVariable(value: any): value is string {
return typeof value === "string" && value.startsWith("var(--")
}
/**
* Parse Framer's special CSS variable format into a CSS token and a fallback.
*
* ```
* `var(--foo, #fff)` => [`--foo`, '#fff']
* ```
*
* @param current
*/
export const cssVariableRegex = /var\((--[a-zA-Z0-9-_]+),? ?([a-zA-Z0-9 ()%#.,-]+)?\)/
export function parseCSSVariable(current: string) {
const match = cssVariableRegex.exec(current)
if (!match) return [,]
const [, token, fallback] = match
return [token, fallback]
}
const maxDepth = 4
function getVariableValue(
current: string,
element: HTMLElement,
depth = 1
): string | undefined {
invariant(
depth <= maxDepth,
`Max CSS variable fallback depth detected in property "${current}". This may indicate a circular fallback dependency.`
)
const [token, fallback] = parseCSSVariable(current)
// No CSS variable detected
if (!token) return
// Attempt to read this CSS variable off the element
const resolved = window.getComputedStyle(element).getPropertyValue(token)
if (resolved) {
return resolved.trim()
} else if (isCSSVariable(fallback)) {
// The fallback might itself be a CSS variable, in which case we attempt to resolve it too.
return getVariableValue(fallback, element, depth + 1)
} else {
return fallback
}
}
/**
* Resolve CSS variables from
*
* @internal
*/
export function resolveCSSVariables(
visualElement: VisualElement,
{ ...target }: TargetWithKeyframes,
transitionEnd: Target | undefined
): { target: TargetWithKeyframes; transitionEnd?: Target } {
const element = visualElement.getInstance()
if (!(element instanceof HTMLElement)) return { target, transitionEnd }
// If `transitionEnd` isn't `undefined`, clone it. We could clone `target` and `transitionEnd`
// only if they change but I think this reads clearer and this isn't a performance-critical path.
if (transitionEnd) {
transitionEnd = { ...transitionEnd }
}
// Go through existing `MotionValue`s and ensure any existing CSS variables are resolved
visualElement.forEachValue((value) => {
const current = value.get()
if (!isCSSVariable(current)) return
const resolved = getVariableValue(current, element)
if (resolved) value.set(resolved)
})
// Cycle through every target property and resolve CSS variables. Currently
// we only read single-var properties like `var(--foo)`, not `calc(var(--foo) + 20px)`
for (const key in target) {
const current = target[key]
if (!isCSSVariable(current)) continue
const resolved = getVariableValue(current, element)
if (!resolved) continue
// Clone target if it hasn't already been
target[key] = resolved
// If the user hasn't already set this key on `transitionEnd`, set it to the unresolved
// CSS variable. This will ensure that after the animation the component will reflect
// changes in the value of the CSS variable.
if (transitionEnd) transitionEnd[key] ??= current
}
return { target, transitionEnd }
}