Skip to content

Commit

Permalink
fix: motion component not using and merging custom presets (#205)
Browse files Browse the repository at this point in the history
* fix: motion component not using and merging custom presets

* fix: always provide custom presets even if none were configured
  • Loading branch information
BobbieGoede authored Sep 4, 2024
1 parent 5f0119e commit c23efca
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 17 deletions.
15 changes: 9 additions & 6 deletions src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,13 @@ import * as presets from '../presets'
import { directive } from '../directive'
import { slugify } from '../utils/slugify'
import { MotionComponent, MotionGroupComponent } from '../components'
import { CUSTOM_PRESETS } from '../utils/keys'

export const MotionPlugin: Plugin = {
install(app, options: MotionPluginOptions<string>) {
// Register default `v-motion` directive
app.directive('motion', directive())

// Register <Motion> component
app.component('Motion', MotionComponent)

// Register <MotionGroup> component
app.component('MotionGroup', MotionGroupComponent)

// Register presets
if (!options || (options && !options.excludePresets)) {
for (const key in presets) {
Expand Down Expand Up @@ -45,6 +40,14 @@ export const MotionPlugin: Plugin = {
app.directive(`motion-${key}`, directive(variants, true))
}
}

app.provide(CUSTOM_PRESETS, options?.directives)

// Register <Motion> component
app.component('Motion', MotionComponent)

// Register <MotionGroup> component
app.component('MotionGroup', MotionGroupComponent)
},
}

Expand Down
40 changes: 30 additions & 10 deletions src/utils/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {
type PropType,
type VNode,
computed,
inject,
nextTick,
onUpdated,
reactive,
toRaw,
} from 'vue'
import type { LooseRequired } from '@vue/shared'
import defu from 'defu'
Expand All @@ -18,20 +20,15 @@ import type {
} from '../types/variants'
import { useMotion } from '../useMotion'
import { variantToStyle } from './transform'

/**
* Type guard, checks if passed string is an existing preset
*/
const isPresetKey = (val: string): val is keyof typeof presets => val in presets
import { CUSTOM_PRESETS } from './keys'

/**
* Shared component props for <Motion> and <MotionGroup>
*/
export const MotionComponentProps = {
// Preset to be loaded
preset: {
type: String as PropType<keyof typeof presets>,
validator: (val: string) => isPresetKey(val),
type: String as PropType<string>,
required: false,
},
// Instance
Expand Down Expand Up @@ -125,10 +122,24 @@ export function setupMotionComponent(
[key: number]: MotionInstance<string, MotionVariants<string>>
}>({})

const customPresets = inject<Record<string, Variant>>(CUSTOM_PRESETS)

// Preset variant or empty object if none is provided
const preset = computed(() =>
props.preset ? structuredClone(presets[props.preset]) : {},
)
const preset = computed(() => {
if (props.preset == null) {
return {}
}

if (customPresets != null && props.preset in customPresets) {
return structuredClone(toRaw(customPresets)[props.preset])
}

if (props.preset in presets) {
return structuredClone(presets[props.preset as keyof typeof presets])
}

return {}
})

// Motion configuration using inline prop variants (`:initial` ...)
const propsConfig = computed(() => ({
Expand Down Expand Up @@ -185,6 +196,15 @@ export function setupMotionComponent(

// Replay animations on component update Vue
if (import.meta.env.DEV) {
// Validate passed preset
if (
props.preset != null
&& presets?.[props.preset as keyof typeof presets] == null
&& customPresets?.[props.preset] == null
) {
console.warn(`[@vueuse/motion]: Preset \`${props.preset}\` not found.`)
}

const replayAnimation = (instance: MotionInstance<any, any>) => {
if (instance.variants?.initial) {
instance.set('initial')
Expand Down
3 changes: 3 additions & 0 deletions src/utils/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const CUSTOM_PRESETS = Symbol(
import.meta.dev ? 'motionCustomPresets' : '',
)
38 changes: 37 additions & 1 deletion tests/components.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ import { intersect } from './utils/intersectionObserver'
import { getTestComponent, useCompletionFn, waitForMockCalls } from './utils'

// Register plugin
config.global.plugins.push(MotionPlugin)
config.global.plugins.push([
MotionPlugin,
{
directives: {
'custom-preset': {
initial: { scale: 1, y: 50 },
hovered: { scale: 1.2, y: 0 },
},
},
},
])

describe.each([
{ t: 'directive', name: '`v-motion` directive (shared tests)' },
Expand Down Expand Up @@ -137,6 +147,32 @@ describe.each([
})

describe('`<Motion>` component', async () => {
it('uses and merges custom presets', async () => {
const wrapper = mount(
{ render: () => h(MotionComponent) },
{
props: {
preset: 'custom-preset',
hovered: { y: 100 },
duration: 10,
},
},
)

const el = wrapper.element as HTMLDivElement
await nextTick()

// Renders initial
expect(el.style.transform).toMatchInlineSnapshot(`"translate3d(0px,50px,0px) scale(1)"`)

// Trigger hovered
await wrapper.trigger('mouseenter')
await new Promise(resolve => setTimeout(resolve, 100))

// `custom-preset` sets scale: 1.2 and `hovered` prop sets y: 100
expect(el.style.transform).toMatchInlineSnapshot(`"translate3d(0px,100px,0px) scale(1.2)"`)
})

it('#202 - preserve variant style on rerender', async () => {
const counter = ref(0)

Expand Down

0 comments on commit c23efca

Please sign in to comment.