Skip to content
1 change: 0 additions & 1 deletion docs/components/demo/reorder-layout/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ function add() {
class="tabs"
>
<AnimatePresence
multiple
:initial="false"
>
<Tab
Expand Down
2 changes: 2 additions & 0 deletions packages/motion/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export * from './animate-presence'
export * from './motion-config'
export * from './reorder'
export { default as RowValue } from './RowValue.vue'
export * from './lazy-motion'
export * from './motion/m'
export { mountedStates } from '@/state'
190 changes: 190 additions & 0 deletions packages/motion/src/components/lazy-motion/__tests__/lazy.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { motionValue } from 'framer-motion/dom'
import { describe, expect, it } from 'vitest'
import { m } from '../../motion/m'
import { motion } from '../../motion'
import { render } from '@testing-library/vue'
import { defineComponent } from 'vue'
import { LazyMotion } from '@/components/lazy-motion'
import { domAnimation, domMax } from '@/features'

describe('lazy feature loading', () => {
it('doesn\'t animate without loaded features', async () => {
const promise = new Promise((resolve) => {
const x = motionValue(0)
const onComplete = () => resolve(x.get())
const Component = defineComponent({
setup() {
return () => (
<m.div
animate={{ x: 20 }}
transition={{ duration: 0.01 }}
style={{ x }}
onAnimationComplete={onComplete}
/>
)
},
})

const wrapper = render(Component)
setTimeout(() => resolve(x.get()), 50)
})
return expect(promise).resolves.not.toBe(20)
})

it('does animate with synchronously-loaded domAnimation', async () => {
const promise = new Promise((resolve) => {
const x = motionValue(0)
const onComplete = () => resolve(x.get())

const Component = defineComponent({
setup() {
return () => (
<LazyMotion features={domAnimation}>
<m.div
animate={{ x: 20 }}
transition={{ duration: 0.01 }}
style={{ x }}
onAnimationComplete={onComplete}
/>
</LazyMotion>
)
},
})

const wrapper = render(Component)
// setTimeout(() => resolve(x.get()), 50)
})

return expect(promise).resolves.toBe(20)
})

it('does animate with synchronously-loaded domMax', async () => {
const promise = new Promise((resolve) => {
const x = motionValue(0)
const onComplete = () => resolve(x.get())

const Component = defineComponent({
setup() {
return () => (
<LazyMotion features={domMax}>
<m.div
animate={{ x: 20 }}
transition={{ duration: 0.01 }}
style={{ x }}
onAnimationComplete={onComplete}
/>
</LazyMotion>
)
},
})

const wrapper = render(Component)
})

return expect(promise).resolves.toBe(20)
})

it('supports nested feature sets', async () => {
const promise = new Promise((resolve) => {
const x = motionValue(0)
const onComplete = () => resolve(x.get())

const Component = defineComponent({
setup() {
return () => (
<LazyMotion features={domMax}>
<m.div
animate={{ x: 20 }}
transition={{ duration: 0.01 }}
style={{ x }}
onAnimationComplete={onComplete}
/>
</LazyMotion>
)
},
})

const wrapper = render(Component)
})

return expect(promise).resolves.toBe(20)
})

it('doesn\'t throw without strict mode', async () => {
const promise = new Promise((resolve) => {
const x = motionValue(0)
const onComplete = () => resolve(x.get())

const Component = defineComponent({
setup() {
return () => (
<LazyMotion features={domMax}>
<m.div
animate={{ x: 20 }}
transition={{ duration: 0.01 }}
style={{ x }}
onAnimationComplete={onComplete}
/>
</LazyMotion>
)
},
})

const wrapper = render(Component)
})

return expect(promise).resolves.toBe(20)
})

it('throws in strict mode', async () => {
const promise = new Promise((resolve) => {
const x = motionValue(0)
const onComplete = () => resolve(x.get())

const Component = defineComponent({
setup() {
return () => (
<LazyMotion features={domMax} strict>
<motion.div
animate={{ x: 20 }}
transition={{ duration: 0.01 }}
style={{ x }}
onAnimationComplete={onComplete}
/>
</LazyMotion>
)
},
})

const wrapper = render(Component)
})

return expect(promise).rejects.toThrowError()
})

it('animates after async loading', async () => {
const promise = new Promise((resolve) => {
const x = motionValue(0)
const onComplete = () => resolve(x.get())

const Component = defineComponent({
setup() {
return () => (
<LazyMotion features={() => import('@/features/dom-animation').then(mod => mod.domAnimation)}>
<m.div
animate={{ x: 20 }}
transition={{ duration: 0.01 }}
style={{ x }}
onAnimationComplete={onComplete}
/>
</LazyMotion>
)
},
})

const wrapper = render(Component)
})

return expect(promise).resolves.toBe(20)
})
})
9 changes: 9 additions & 0 deletions packages/motion/src/components/lazy-motion/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Feature } from '@/features'
import { createContext } from '@/utils'
import type { Ref } from 'vue'

export type LazyMotionContext = {
features: Ref<Feature[]>
strict: boolean
}
export const [useLazyMotionContext, lazyMotionContextProvider] = createContext<LazyMotionContext>('LazyMotionContext')
35 changes: 35 additions & 0 deletions packages/motion/src/components/lazy-motion/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { lazyMotionContextProvider } from '@/components/lazy-motion/context'
import type { Feature } from '@/features'
import type { PropType } from 'vue'
import { defineComponent, ref } from 'vue'

export const LazyMotion = defineComponent({
name: 'LazyMotion',
inheritAttrs: false,
props: {
features: {
type: [Object, Function] as PropType<Feature[] | Promise<Feature[]> | (() => Promise<Feature[]>)>,
default: () => ([]),
},
strict: {
type: Boolean,
default: false,
},
},
setup(props, { slots }) {
const features = ref<any[]>(Array.isArray(props.features) ? props.features : [])
if (!Array.isArray(props.features)) {
const featuresPromise = typeof props.features === 'function' ? props.features() : props.features
featuresPromise.then((feats) => {
features.value = feats
})
}
lazyMotionContextProvider({
features,
strict: props.strict,
})
return () => {
return slots.default?.()
}
},
})
Loading