From 2efc8a6cd7f04940ebf06beef65703b6adf785cb Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Fri, 11 Sep 2026 16:29:56 +0100 Subject: [PATCH] fix: accept SVG elements in autoAnimate TypeScript types The core already tracks parents as Element and animates SVG at runtime, but the public signature only allowed HTMLElement, which broke Svelte actions and TS callers (issue #100). Widen the contract to HTMLElement | SVGElement and stop adapter instanceof HTMLElement gates from silently skipping SVG nodes. --- src/index.ts | 10 +++++----- src/marko/auto-animate.marko | 2 +- src/preact/index.ts | 5 ++++- src/qwik/index.ts | 2 +- src/react/index.ts | 2 +- src/solid/index.ts | 4 ++-- src/vue/index.ts | 21 ++++++++++++++------- tests/e2e/svg-types.spec.ts | 23 +++++++++++++++++++++++ tests/types/svg-element.ts | 17 +++++++++++++++++ tests/types/tsconfig.json | 9 +++++++++ 10 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 tests/e2e/svg-types.spec.ts create mode 100644 tests/types/svg-element.ts create mode 100644 tests/types/tsconfig.json diff --git a/src/index.ts b/src/index.ts index 772b21f7..d6eb4e3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -647,7 +647,7 @@ function cleanUp(el: Element, styles?: Partial) { setTimeout(() => { if (DEL in el) delete (el as any)[DEL] Object.defineProperty(el, NEW, { value: true, configurable: true }) - if (styles && el instanceof HTMLElement) { + if (styles && (el instanceof HTMLElement || el instanceof SVGElement)) { for (const style in styles) { ;(el.style as any)[style as any] = "" } @@ -877,11 +877,11 @@ export interface AutoAnimationPlugin { * A function that automatically adds animation effects to itself and its * immediate children. Specifically it adds effects for adding, moving, and * removing DOM elements. - * @param el - A parent element to add animations to. + * @param el - An HTML or SVG parent element to add animations to. * @param options - An optional object of options. */ export default function autoAnimate( - el: HTMLElement, + el: HTMLElement | SVGElement, config: Partial | AutoAnimationPlugin = {}, ): AnimationController { if (supportedBrowser && resize) { @@ -978,7 +978,7 @@ export { autoAnimate } */ export const vAutoAnimate = { mounted: ( - el: HTMLElement, + el: HTMLElement | SVGElement, binding: { value: Partial | AutoAnimationPlugin | undefined }, @@ -986,7 +986,7 @@ export const vAutoAnimate = { const ctl = autoAnimate(el, binding.value || {}) Object.defineProperty(el, "__aa_ctl", { value: ctl, configurable: true }) }, - unmounted: (el: HTMLElement) => { + unmounted: (el: HTMLElement | SVGElement) => { const ctl = (el as any)["__aa_ctl"] as AnimationController | undefined ctl?.destroy?.() try { diff --git a/src/marko/auto-animate.marko b/src/marko/auto-animate.marko index 39d17ffe..1c05b8f8 100644 --- a/src/marko/auto-animate.marko +++ b/src/marko/auto-animate.marko @@ -8,7 +8,7 @@ import type { export interface Input { // A reference to the parent element whose *direct children* will be animated. // Pass a native element tag-variable, e.g. `
    ` then `parent=listRef`. - parent: () => HTMLElement; + parent: () => HTMLElement | SVGElement; // auto-animate options (duration / easing / disrespectUserMotionPreference) // OR a plugin function. Read once when the tag mounts; the core has no setOptions, // so changing this later has no effect (remount the tag to apply new options). diff --git a/src/preact/index.ts b/src/preact/index.ts index 7710e601..5fa79885 100644 --- a/src/preact/index.ts +++ b/src/preact/index.ts @@ -23,7 +23,10 @@ export function useAutoAnimate( } } useEffect(() => { - if (element.current instanceof HTMLElement) + if ( + element.current instanceof HTMLElement || + element.current instanceof SVGElement + ) setController(autoAnimate(element.current, options || {})) }, []) useEffect(() => { diff --git a/src/qwik/index.ts b/src/qwik/index.ts index 831aea93..e9dfaddb 100644 --- a/src/qwik/index.ts +++ b/src/qwik/index.ts @@ -20,7 +20,7 @@ import autoAnimate, { * @param options - Auto animate options or a plugin * @returns */ -export function useAutoAnimate( +export function useAutoAnimate( options?: Partial | AutoAnimationPlugin ): [ Signal, diff --git a/src/react/index.ts b/src/react/index.ts index 76b40835..cbcddf35 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -19,7 +19,7 @@ export function useAutoAnimate( const memoizedOptions = useMemo(() => options, []) const element = useCallback( (node: T) => { - if (node instanceof HTMLElement) { + if (node instanceof HTMLElement || node instanceof SVGElement) { setController(autoAnimate(node, memoizedOptions)) } else { setController(undefined) diff --git a/src/solid/index.ts b/src/solid/index.ts index d627b872..436c8a72 100644 --- a/src/solid/index.ts +++ b/src/solid/index.ts @@ -13,7 +13,7 @@ declare module "solid-js" { } } -export const createAutoAnimate = ( +export const createAutoAnimate = ( options: Partial | AutoAnimationPlugin = {} ): [Setter, (enabled: boolean) => void] => { const [element, setElement] = createSignal(null) @@ -43,7 +43,7 @@ export const createAutoAnimate = ( export const createAutoAnimateDirective = () => { return ( - el: HTMLElement, + el: HTMLElement | SVGElement, options: Accessor | AutoAnimationPlugin | true> ) => { let optionsValue = options() diff --git a/src/vue/index.ts b/src/vue/index.ts index e225cc18..0915fdb2 100644 --- a/src/vue/index.ts +++ b/src/vue/index.ts @@ -8,10 +8,10 @@ import autoAnimate, { } from "../index" export const vAutoAnimate: Directive< - HTMLElement | Component, + HTMLElement | SVGElement | Component, Partial > = autoAnimateDirective as unknown as Directive< - HTMLElement | Component, + HTMLElement | SVGElement | Component, Partial > @@ -20,7 +20,10 @@ export const vAutoAnimate: Directive< */ export function createVAutoAnimate( defaults?: Partial | AutoAnimationPlugin -): Directive | AutoAnimationPlugin> { +): Directive< + HTMLElement | SVGElement, + Partial | AutoAnimationPlugin +> { return { mounted(el, binding) { let resolved: Partial | AutoAnimationPlugin = {} @@ -44,7 +47,7 @@ export function createVAutoAnimate( }, getSSRProps: () => ({}), } as unknown as Directive< - HTMLElement, + HTMLElement | SVGElement, Partial | AutoAnimationPlugin > } @@ -73,13 +76,17 @@ export function useAutoAnimate( } onMounted(() => { watchEffect((onCleanup) => { - let el: HTMLElement | undefined - if (element.value instanceof HTMLElement) { + let el: HTMLElement | SVGElement | undefined + if ( + element.value instanceof HTMLElement || + element.value instanceof SVGElement + ) { el = element.value } else if ( element.value && "$el" in element.value && - element.value.$el instanceof HTMLElement + (element.value.$el instanceof HTMLElement || + element.value.$el instanceof SVGElement) ) { el = element.value.$el } diff --git a/tests/e2e/svg-types.spec.ts b/tests/e2e/svg-types.spec.ts new file mode 100644 index 00000000..3be3b6e8 --- /dev/null +++ b/tests/e2e/svg-types.spec.ts @@ -0,0 +1,23 @@ +import { test, expect } from '@playwright/test' +import { execFileSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' +import path from 'node:path' + +test.describe('SVG type assignability (issue #100)', () => { + test('autoAnimate and vAutoAnimate accept SVG elements', () => { + const root = fileURLToPath(new URL('../..', import.meta.url)) + const tsc = path.join(root, 'node_modules/typescript/bin/tsc') + let output = '' + try { + output = execFileSync( + tsc, + ['--pretty', 'false', '-p', 'tests/types/tsconfig.json'], + { encoding: 'utf8', cwd: root }, + ) + } catch (err) { + const error = err as { stdout?: string; stderr?: string } + throw new Error(error.stdout || error.stderr || String(err)) + } + expect(output).toBe('') + }) +}) diff --git a/tests/types/svg-element.ts b/tests/types/svg-element.ts new file mode 100644 index 00000000..56fff9a9 --- /dev/null +++ b/tests/types/svg-element.ts @@ -0,0 +1,17 @@ +import autoAnimate, { vAutoAnimate } from "../../src/index" + +declare const svg: SVGSVGElement +declare const html: HTMLElement +declare const group: SVGGElement + +autoAnimate(svg) +autoAnimate(html) +autoAnimate(group) + +const binding = { + value: undefined as undefined, +} +vAutoAnimate.mounted(svg, binding) +vAutoAnimate.mounted(html, binding) +vAutoAnimate.unmounted(svg) +vAutoAnimate.unmounted(html) diff --git a/tests/types/tsconfig.json b/tests/types/tsconfig.json new file mode 100644 index 00000000..a2d84c2e --- /dev/null +++ b/tests/types/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": ["./svg-element.ts"] +}