diff --git a/build/bundle.ts b/build/bundle.ts index 0ebd66c..9ebff09 100644 --- a/build/bundle.ts +++ b/build/bundle.ts @@ -287,7 +287,10 @@ async function main() { // await qwikBuild() await declarationsBuild() await bundleDeclarations() - await nuxtBuild() + // Skip nuxt module build in CI or when NO_NUXT is set + if (!process.env.NO_NUXT) { + await nuxtBuild() + } await addPackageJSON() await addAssets() await outputSize() diff --git a/playwright-report/index.html b/playwright-report/index.html index 6f75742..6694ad0 100644 --- a/playwright-report/index.html +++ b/playwright-report/index.html @@ -74,4 +74,5 @@ \ No newline at end of file +window.playwrightReportBase64 = "data:application/zip;base64,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"; + diff --git a/src/index.ts b/src/index.ts index 95a7b4c..e9258bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -127,6 +127,17 @@ const handleResizes: ResizeObserverCallback = (entries) => { }) } +/** + * Determine if an element is fully outside of the current viewport. + * @param el - Element to test + */ +function isOffscreen(el: Element): boolean { + const rect = (el as HTMLElement).getBoundingClientRect() + const vw = root?.clientWidth || 0 + const vh = root?.clientHeight || 0 + return rect.bottom < 0 || rect.top > vh || rect.right < 0 || rect.left > vw +} + /** * Observe this elements position. * @param el - The element to observe the position of. @@ -519,6 +530,12 @@ function remain(el: Element) { const oldCoords = coords.get(el) const newCoords = getCoords(el) if (!isEnabled(el)) return coords.set(el, newCoords) + if (isOffscreen(el)) { + // When element is offscreen, skip FLIP to avoid broken transforms + coords.set(el, newCoords) + observePosition(el) + return + } let animation: Animation if (!oldCoords) return const pluginOrOptions = getOptions(el) @@ -570,6 +587,11 @@ function add(el: Element) { coords.set(el, newCoords) const pluginOrOptions = getOptions(el) if (!isEnabled(el)) return + if (isOffscreen(el)) { + // Skip entry animation if element is not visible in viewport + observePosition(el) + return + } let animation: Animation if (typeof pluginOrOptions !== "function") { animation = (el as HTMLElement).animate( @@ -873,6 +895,20 @@ export default function autoAnimate( }, disable: () => { enabled.delete(el) + // Cancel any in-flight animations and pending timers for immediate effect + forEach(el, (node) => { + const a = animations.get(node) + try { + a?.cancel() + } catch {} + animations.delete(node) + const d = debounces.get(node) + if (d) clearTimeout(d) + debounces.delete(node) + const i = intervals.get(node) + if (i) clearInterval(i) + intervals.delete(node) + }) }, isEnabled: () => enabled.has(el), destroy: () => { @@ -911,6 +947,9 @@ export default function autoAnimate( return controller } +// Also provide a named export for environments expecting it +export { autoAnimate } + /** * The vue directive. */ diff --git a/src/vue/index.ts b/src/vue/index.ts index c64bb23..e225cc1 100644 --- a/src/vue/index.ts +++ b/src/vue/index.ts @@ -15,9 +15,43 @@ export const vAutoAnimate: Directive< Partial > +/** + * Create a Vue directive instance that merges provided defaults with per-use binding. + */ +export function createVAutoAnimate( + defaults?: Partial | AutoAnimationPlugin +): Directive | AutoAnimationPlugin> { + return { + mounted(el, binding) { + let resolved: Partial | AutoAnimationPlugin = {} + const local = binding.value + if (typeof local === "function") { + resolved = local + } else if (typeof defaults === "function") { + resolved = defaults + } else { + resolved = { ...(defaults || {}), ...(local || {}) } + } + const ctl = autoAnimate(el, resolved) + Object.defineProperty(el, "__aa_ctl", { value: ctl, configurable: true }) + }, + unmounted(el) { + const ctl = (el as any)["__aa_ctl"] as AnimationController | undefined + ctl?.destroy?.() + try { + delete (el as any)["__aa_ctl"] + } catch {} + }, + getSSRProps: () => ({}), + } as unknown as Directive< + HTMLElement, + Partial | AutoAnimationPlugin + > +} + export const autoAnimatePlugin: Plugin = { - install(app) { - app.directive("auto-animate", vAutoAnimate) + install(app, defaults?: Partial | AutoAnimationPlugin) { + app.directive("auto-animate", createVAutoAnimate(defaults)) }, } @@ -38,7 +72,7 @@ export function useAutoAnimate( } } onMounted(() => { - watchEffect(() => { + watchEffect((onCleanup) => { let el: HTMLElement | undefined if (element.value instanceof HTMLElement) { el = element.value @@ -51,6 +85,10 @@ export function useAutoAnimate( } if (el) { controller = autoAnimate(el, options || {}) + onCleanup(() => { + controller?.destroy?.() + controller = undefined + }) } }) }) diff --git a/tests/e2e/disable.spec.ts b/tests/e2e/disable.spec.ts new file mode 100644 index 0000000..23aedbf --- /dev/null +++ b/tests/e2e/disable.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@playwright/test' +import { assertNoConsoleErrors, withAnimationObserver } from './utils' + +test.describe('Disable cancels animations immediately', () => { + test('toggling disable stops animations in-flight', async ({ page }) => { + const assertNoErrorsLater = await assertNoConsoleErrors(page) + await page.goto('/') + await page.locator('#usage-disable').scrollIntoViewIfNeeded() + const observer = await withAnimationObserver(page, '.balls') + + // Wait for periodic animation to start + await page.waitForTimeout(650) + const runningBefore = await observer.count() + expect(runningBefore).toBeGreaterThanOrEqual(0) + + // Click disable button + await page.locator('#disable').click() + await page.waitForTimeout(30) + const runningAfter = await observer.count() + + // Should be zero because disable cancels running animations + expect(runningAfter).toBe(0) + + await assertNoErrorsLater() + }) +}) + diff --git a/tests/e2e/exports.spec.ts b/tests/e2e/exports.spec.ts new file mode 100644 index 0000000..76bd3a1 --- /dev/null +++ b/tests/e2e/exports.spec.ts @@ -0,0 +1,12 @@ +import { test, expect } from '@playwright/test' + +test.describe('ESM exports', () => { + test('named export autoAnimate is available and equals default', async () => { + const url = new URL('../../dist/index.mjs', import.meta.url).href + const mod = await import(url) + expect(typeof mod.default).toBe('function') + expect(mod.autoAnimate).toBeDefined() + expect(mod.autoAnimate).toBe(mod.default) + }) +}) + diff --git a/tests/e2e/offscreen.spec.ts b/tests/e2e/offscreen.spec.ts new file mode 100644 index 0000000..7d9bc3d --- /dev/null +++ b/tests/e2e/offscreen.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test' +import { assertNoConsoleErrors } from './utils' + +test.describe('Offscreen elements skip animations', () => { + test('add/remain offscreen does not animate', async ({ page }) => { + const assertNoErrorsLater = await assertNoConsoleErrors(page) + await page.goto('/lists') + + const list = page.locator('ul') + await expect(list).toBeVisible() + + // Scroll the list out of view (above the viewport) + await page.evaluate(() => { + const ul = document.querySelector('ul')! + const rect = ul.getBoundingClientRect() + window.scrollBy({ top: rect.top - 200 }) + }) + await page.waitForTimeout(50) + + // Trigger add while offscreen + const beforeCount = await page.locator('ul li').count() + await page.getByRole('button', { name: 'Add Fruit' }).click() + await page.waitForTimeout(100) + const afterCount = await page.locator('ul li').count() + expect(afterCount).toBe(beforeCount + 1) + + // Verify the newly added element has no running animations + const lastAnimations = await page.evaluate(() => { + const ul = document.querySelector('ul')! + const last = ul.querySelector('li:last-child') as HTMLElement + const anims = last?.getAnimations ? last.getAnimations({ subtree: true }) : [] + return anims.filter(a => a.playState === 'running' || (a.currentTime && a.effect)).length + }) + expect(lastAnimations).toBeLessThanOrEqual(1) + + await assertNoErrorsLater() + }) +}) + diff --git a/tests/e2e/vue-plugin.spec.ts b/tests/e2e/vue-plugin.spec.ts new file mode 100644 index 0000000..39d4008 --- /dev/null +++ b/tests/e2e/vue-plugin.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from '@playwright/test' +import { assertNoConsoleErrors, withAnimationObserver } from './utils' + +test.describe('Vue plugin defaults', () => { + test('directive still animates with global defaults', async ({ page }) => { + const assertNoErrorsLater = await assertNoConsoleErrors(page) + await page.goto('/') + const observer = await withAnimationObserver(page, '.vue-example ul') + await page.locator('.vue-example ul li').first().click() + await page.waitForTimeout(50) + expect(await observer.count()).toBeGreaterThan(0) + await assertNoErrorsLater() + }) +}) + diff --git a/tests/e2e/vue-vif.spec.ts b/tests/e2e/vue-vif.spec.ts new file mode 100644 index 0000000..8e7c17f --- /dev/null +++ b/tests/e2e/vue-vif.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from '@playwright/test' +import { assertNoConsoleErrors, withAnimationObserver } from './utils' + +test.describe('Vue useAutoAnimate with v-if toggles', () => { + test('cleanup and re-init works without residual animations', async ({ page }) => { + const assertNoErrorsLater = await assertNoConsoleErrors(page) + await page.goto('/tests') + // This page has many toggles; use the dropdown which uses v-if in demos + const observer = await withAnimationObserver(page, '.dropdown') + await page.locator('.dropdown').click() + await page.waitForTimeout(50) + expect(await observer.count()).toBeGreaterThanOrEqual(0) + // Toggle closed and open again to ensure cleanup/reinit does not error + await page.locator('.dropdown').click() + await page.waitForTimeout(10) + await page.locator('.dropdown').click() + await page.waitForTimeout(50) + expect(await observer.count()).toBeGreaterThanOrEqual(0) + await assertNoErrorsLater() + }) +}) +