Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions packages/framer-motion/src/animation/animate/resolve-subjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,29 @@ import { ObjectTarget } from "../sequence/types"
import { isDOMKeyframes } from "../utils/is-dom-keyframes"

export function resolveSubjects<O extends {}>(
subject: string | Element | Element[] | NodeListOf<Element> | O | O[],
subject:
| string
| Element
| Element[]
| NodeListOf<Element>
| O
| O[]
| null
| undefined,
keyframes: DOMKeyframesDefinition | ObjectTarget<O>,
scope?: AnimationScope,
selectorCache?: SelectorCache
) {
if (subject == null) {
return []
}

if (typeof subject === "string" && isDOMKeyframes(keyframes)) {
return resolveElements(subject, scope, selectorCache)
} else if (subject instanceof NodeList) {
return Array.from(subject)
} else if (Array.isArray(subject)) {
return subject
return subject.filter((s) => s != null)
} else {
return [subject]
}
Expand Down
11 changes: 5 additions & 6 deletions packages/framer-motion/src/animation/animate/subject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ export function animateSubject<O extends Object>(
)
)
} else {
// Gracefully handle null/undefined subjects (e.g., from querySelector returning null)
if (subject == null) {
return animations
}

const subjects = resolveSubjects(
subject,
keyframes as DOMKeyframesDefinition,
Expand All @@ -124,12 +129,6 @@ export function animateSubject<O extends Object>(
for (let i = 0; i < numSubjects; i++) {
const thisSubject = subjects[i]

invariant(
thisSubject !== null,
"You're trying to perform an animation on null. Ensure that selectors are correctly finding elements and refs are correctly hydrated.",
"animate-null"
)

const createVisualElement =
thisSubject instanceof Element
? createDOMVisualElement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export function animateElements(
options?: DynamicAnimationOptions,
scope?: AnimationScope
) {
// Gracefully handle null/undefined elements (e.g., from querySelector returning null)
if (elementOrSelector == null) {
return []
}

const elements = resolveElements(elementOrSelector, scope) as Array<
HTMLElement | SVGElement
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -768,4 +768,48 @@ describe("createAnimationsFromSequence", () => {
expect(duration).toEqual(4)
expect(times).toEqual([0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1])
})

test("It skips null elements in sequence", () => {
const animations = createAnimationsFromSequence(
[
[a, { opacity: 1 }, { duration: 1 }],
[null as unknown as Element, { opacity: 0.5 }, { duration: 1 }],
[b, { opacity: 0 }, { duration: 1 }],
],
undefined,
undefined,
{ spring }
)

// Should only have animations for a and b, not the null element
expect(animations.size).toBe(2)
expect(animations.has(a)).toBe(true)
expect(animations.has(b)).toBe(true)
})

test("It filters null elements from array of targets", () => {
const animations = createAnimationsFromSequence(
[[[a, null as unknown as Element, b], { x: 100 }, { duration: 1 }]],
undefined,
undefined,
{ spring }
)

// Should only have animations for a and b, not the null element
expect(animations.size).toBe(2)
expect(animations.has(a)).toBe(true)
expect(animations.has(b)).toBe(true)
})

test("It handles sequence with only null element gracefully", () => {
const animations = createAnimationsFromSequence(
[[null as unknown as Element, { opacity: 1 }, { duration: 1 }]],
undefined,
undefined,
{ spring }
)

// Should return empty map when no valid elements
expect(animations.size).toBe(0)
})
})
10 changes: 9 additions & 1 deletion packages/motion-dom/src/utils/resolve-elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export type ElementOrSelector =
| Element[]
| NodeListOf<Element>
| string
| null
| undefined

export interface WithQuerySelectorAll {
querySelectorAll: Element["querySelectorAll"]
Expand All @@ -22,6 +24,10 @@ export function resolveElements(
scope?: AnimationScope,
selectorCache?: SelectorCache
): Element[] {
if (elementOrSelector == null) {
return []
}

if (elementOrSelector instanceof EventTarget) {
return [elementOrSelector]
} else if (typeof elementOrSelector === "string") {
Expand All @@ -38,5 +44,7 @@ export function resolveElements(
return elements ? Array.from(elements) : []
}

return Array.from(elementOrSelector)
return Array.from(elementOrSelector).filter(
(element): element is Element => element != null
)
}