Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- **ScrollAnimation:** add a `withScrollAnimationDebug` decorator ([#494](https://github.com/studiometa/ui/pull/494))

### Fixed

- **ScrollAnimation:** complete animation to nearest boundary on destroy ([#444](https://github.com/studiometa/ui/issues/444), [#496](https://github.com/studiometa/ui/pull/496), [ecb63dd](https://github.com/studiometa/ui/commit/ecb63dd))

### Changed

- **ScrollAnimation:** refactor components into `ScrollAnimationTimeline` and `ScrollAnimationTarget` ([#441](https://github.com/studiometa/ui/issues/441) [#494](https://github.com/studiometa/ui/pull/494), [a5d0e29](https://github.com/studiometa/ui/commit/a5d0e29))
Expand Down
75 changes: 75 additions & 0 deletions packages/tests/ScrollAnimation/AbstractScrollAnimation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AbstractScrollAnimation } from '@studiometa/ui';
import { nextTick } from '@studiometa/js-toolkit/utils';
import { h, mount, destroy } from '#test-utils';

class TestScrollAnimation extends AbstractScrollAnimation {
Expand Down Expand Up @@ -126,4 +127,78 @@ describe('AbstractScrollAnimation', () => {
animation.render(0.75);
expect(progressSpy).toHaveBeenCalledWith(0.75);
});

it('should track progress in render method', () => {
const progressSpy = vi.fn();
const mockAnimation = { progress: progressSpy };

Object.defineProperty(animation, 'animation', {
value: mockAnimation,
configurable: true,
});

expect(animation.progress).toBe(0);
animation.render(0.75);
expect(animation.progress).toBe(0.75);
});

it('should restore animation state on mount', async () => {
const testElement = h('div', {
dataOptionFrom: JSON.stringify({ opacity: 0 }),
dataOptionTo: JSON.stringify({ opacity: 1 }),
});
const testAnimation = new TestScrollAnimation(testElement);
await mount(testAnimation);

// Simulate partial progress closer to end
testAnimation.render(0.6);
expect(testAnimation.progress).toBe(0.6);

// Destroy rounds progress to nearest boundary (1)
await destroy(testAnimation);
await nextTick();
expect(testAnimation.progress).toBe(1);

// Remount restores the completed progress
await mount(testAnimation);
expect(testAnimation.progress).toBe(1);
});

it('should complete animation to nearest boundary on destroy', async () => {
const testElement = h('div', {
dataOptionFrom: JSON.stringify({ opacity: 0 }),
dataOptionTo: JSON.stringify({ opacity: 1 }),
});
const testAnimation = new TestScrollAnimation(testElement);
await mount(testAnimation);

// Simulate partial progress closer to 1
testAnimation.render(0.8);
expect(testAnimation.progress).toBe(0.8);

// Destroy should trigger completion to nearest boundary (1)
await destroy(testAnimation);
await nextTick();

expect(testAnimation.progress).toBe(1);
});

it('should complete animation to 0 when progress is closer to start', async () => {
const testElement = h('div', {
dataOptionFrom: JSON.stringify({ opacity: 0 }),
dataOptionTo: JSON.stringify({ opacity: 1 }),
});
const testAnimation = new TestScrollAnimation(testElement);
await mount(testAnimation);

// Simulate partial progress closer to 0
testAnimation.render(0.3);
expect(testAnimation.progress).toBe(0.3);

// Destroy should trigger completion to nearest boundary (0)
await destroy(testAnimation);
await nextTick();

expect(testAnimation.progress).toBe(0);
});
});
27 changes: 26 additions & 1 deletion packages/ui/ScrollAnimation/AbstractScrollAnimation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Base, withFreezedOptions } from '@studiometa/js-toolkit';
import type { BaseProps, BaseConfig, ScrollInViewProps } from '@studiometa/js-toolkit';
import { map, clamp01, animate } from '@studiometa/js-toolkit/utils';
import { map, clamp01, animate, nextTick } from '@studiometa/js-toolkit/utils';
import type { Keyframe } from '@studiometa/js-toolkit/utils';

export interface AbstractScrollAnimationProps extends BaseProps {
Expand Down Expand Up @@ -47,6 +47,30 @@ export class AbstractScrollAnimation<
},
};

/**
* Current animation progress (0 to 1).
*/
progress = 0;

/**
* Constructor.
*/
constructor(element: HTMLElement) {
super(element);

// Restore animation state on mount
this.$on('mounted', () => {
this.render(this.progress);
});

// Complete animation to nearest boundary on destroy
this.$on('destroyed', () => {
nextTick(() => {
this.render(Math.round(this.progress));
});
});
}

/**
* Get the target element for the animation.
*/
Expand Down Expand Up @@ -103,6 +127,7 @@ export class AbstractScrollAnimation<
* Render the animation for the given progress.
*/
render(progress: number) {
this.progress = progress;
this.animation.progress(progress);
}
}
Loading