diff --git a/src/plugin/index.ts b/src/plugin/index.ts index 201a8c37..b20515a8 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -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) { // Register default `v-motion` directive app.directive('motion', directive()) - // Register component - app.component('Motion', MotionComponent) - - // Register component - app.component('MotionGroup', MotionGroupComponent) - // Register presets if (!options || (options && !options.excludePresets)) { for (const key in presets) { @@ -45,6 +40,14 @@ export const MotionPlugin: Plugin = { app.directive(`motion-${key}`, directive(variants, true)) } } + + app.provide(CUSTOM_PRESETS, options?.directives) + + // Register component + app.component('Motion', MotionComponent) + + // Register component + app.component('MotionGroup', MotionGroupComponent) }, } diff --git a/src/utils/component.ts b/src/utils/component.ts index 5a67496b..0f1c7387 100644 --- a/src/utils/component.ts +++ b/src/utils/component.ts @@ -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' @@ -18,11 +20,7 @@ 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 and @@ -30,8 +28,7 @@ const isPresetKey = (val: string): val is keyof typeof presets => val in presets export const MotionComponentProps = { // Preset to be loaded preset: { - type: String as PropType, - validator: (val: string) => isPresetKey(val), + type: String as PropType, required: false, }, // Instance @@ -125,10 +122,24 @@ export function setupMotionComponent( [key: number]: MotionInstance> }>({}) + const customPresets = inject>(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(() => ({ @@ -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) => { if (instance.variants?.initial) { instance.set('initial') diff --git a/src/utils/keys.ts b/src/utils/keys.ts new file mode 100644 index 00000000..9950bdc9 --- /dev/null +++ b/src/utils/keys.ts @@ -0,0 +1,3 @@ +export const CUSTOM_PRESETS = Symbol( + import.meta.dev ? 'motionCustomPresets' : '', +) diff --git a/tests/components.spec.ts b/tests/components.spec.ts index 70095238..1548a169 100644 --- a/tests/components.spec.ts +++ b/tests/components.spec.ts @@ -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)' }, @@ -137,6 +147,32 @@ describe.each([ }) describe('`` 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)