Skip to content

Commit 997e0cb

Browse files
authored
migrating onAbort fix from wow-libs + tests (#163)
* migrating onAbort fix from wow-libs + tests * remove redundant viewEnter test + dispatching event * adding back the test * format
1 parent 35b2d34 commit 997e0cb

5 files changed

Lines changed: 192 additions & 4 deletions

File tree

packages/interact/src/handlers/effectHandlers.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,14 @@ export function createTimeEffectHandler(
6262
animation.progress(0);
6363
delete element.dataset.interactEnter;
6464
if (animation.isCSS) {
65-
animation.onFinish(() => {
65+
const setEnterDone = () => {
6666
fastdom.mutate(() => {
6767
element.dataset.interactEnter = 'done';
6868
});
69-
});
69+
};
70+
71+
animation.onFinish(setEnterDone);
72+
animation.onAbort(setEnterDone);
7073
}
7174
animation.play();
7275
}

packages/interact/src/handlers/viewEnter.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,14 @@ function addViewEnterHandler(
181181
requestAnimationFrame(setEnterStart);
182182
});
183183

184-
animation.onFinish(() => {
184+
const setEnterDone = () => {
185185
fastdom.mutate(() => {
186186
target.dataset.interactEnter = 'done';
187187
});
188-
});
188+
};
189+
190+
animation.onFinish(setEnterDone);
191+
animation.onAbort(setEnterDone);
189192
} else {
190193
fastdom.mutate(setEnterStart);
191194
}

packages/interact/test/viewEnter.spec.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ vi.mock('@wix/motion', () => ({
55
play: vi.fn(),
66
cancel: vi.fn(),
77
onFinish: vi.fn(),
8+
onAbort: vi.fn(),
89
pause: vi.fn(),
910
reverse: vi.fn(),
1011
progress: vi.fn(),
@@ -592,4 +593,80 @@ describe('viewEnter handler', () => {
592593
expect(observeSpy).not.toHaveBeenCalled();
593594
});
594595
});
596+
597+
describe('CSS animation abort handling', () => {
598+
it('should set interactEnter to done when CSS animation is aborted', async () => {
599+
let rejectFinished: (error: DOMException) => void;
600+
const finishedPromise = new Promise((_resolve, reject) => {
601+
rejectFinished = reject;
602+
});
603+
604+
const cssAnimation = {
605+
play: vi.fn(),
606+
ready: Promise.resolve(),
607+
finished: finishedPromise,
608+
cancel: vi.fn(() => {
609+
rejectFinished(new DOMException('The animation was aborted.', 'AbortError'));
610+
}),
611+
};
612+
613+
const mockAnimationGroup = {
614+
animations: [cssAnimation],
615+
isCSS: true,
616+
ready: Promise.resolve(),
617+
async play(callback?: () => void) {
618+
for (const a of this.animations) {
619+
a.play();
620+
}
621+
await Promise.all(this.animations.map((a: any) => a.ready));
622+
callback?.();
623+
},
624+
async onFinish(callback: () => void) {
625+
try {
626+
await Promise.all(this.animations.map((a: any) => a.finished));
627+
callback();
628+
} catch (_error) {
629+
/* empty */
630+
}
631+
},
632+
async onAbort(callback: () => void) {
633+
try {
634+
await Promise.all(this.animations.map((a: any) => a.finished));
635+
} catch (error: any) {
636+
if (error.name === 'AbortError') {
637+
callback();
638+
}
639+
}
640+
},
641+
cancel: vi.fn(),
642+
pause: vi.fn(),
643+
reverse: vi.fn(),
644+
progress: vi.fn(),
645+
persist: vi.fn(),
646+
playState: 'idle',
647+
};
648+
649+
viewEnterHandler.add(
650+
element,
651+
target,
652+
{ duration: 1000, namedEffect: { type: 'FadeIn' } },
653+
{ type: 'once' },
654+
{ animation: mockAnimationGroup as any },
655+
);
656+
657+
const entry = createEntry({ isIntersecting: true });
658+
getMainObserverCallback()([entry]);
659+
660+
// Wait for play's async callback to be invoked
661+
await new Promise((resolve) => setTimeout(resolve, 0));
662+
663+
// Cancel the CSSAnimation directly, simulating the browser aborting the animation
664+
cssAnimation.cancel();
665+
666+
// Wait for onAbort to process the AbortError and set interactEnter
667+
await new Promise((resolve) => setTimeout(resolve, 0));
668+
669+
expect(target.dataset.interactEnter).toBe('done');
670+
});
671+
});
595672
});

packages/motion/src/AnimationGroup.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,27 @@ export class AnimationGroup {
107107
}
108108
}
109109

110+
async onAbort(callback: () => void): Promise<void> {
111+
try {
112+
await Promise.all(this.animations.map((animation) => animation.finished));
113+
} catch (error: any) {
114+
if (error.name === 'AbortError') {
115+
const a = this.animations[0];
116+
117+
if (a && !this.isCSS) {
118+
const target = (a.effect as KeyframeEffect)?.target;
119+
120+
if (target) {
121+
const cancelEvent = new Event('animationcancel');
122+
target.dispatchEvent(cancelEvent);
123+
}
124+
}
125+
126+
callback();
127+
}
128+
}
129+
}
130+
110131
get finished() {
111132
return Promise.all(this.animations.map((animation) => animation.finished));
112133
}

packages/motion/test/AnimationGroup.spec.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,90 @@ describe('AnimationGroup', () => {
929929
});
930930
});
931931

932+
describe('onAbort()', () => {
933+
test('should execute callback when a CSSAnimation is cancelled externally (not via AnimationGroup.cancel)', async () => {
934+
const callback = vi.fn();
935+
let rejectFinished: (error: any) => void;
936+
937+
const finishedPromise = new Promise<Animation>((_resolve, reject) => {
938+
rejectFinished = reject;
939+
});
940+
941+
const mockAnimation = createMockAnimation({
942+
finished: finishedPromise,
943+
cancel: vi.fn(() => {
944+
rejectFinished(new DOMException('The animation was aborted.', 'AbortError'));
945+
}),
946+
});
947+
948+
const animationGroup = new AnimationGroup([mockAnimation]);
949+
const abortPromise = animationGroup.onAbort(callback);
950+
951+
expect(callback).not.toHaveBeenCalled();
952+
953+
// Cancel the inner CSSAnimation directly, simulating the browser
954+
// removing the animation (e.g. a viewEnter effect going out of range)
955+
mockAnimation.cancel();
956+
957+
await abortPromise;
958+
959+
expect(callback).toHaveBeenCalledTimes(1);
960+
});
961+
962+
test('should execute callback when one of multiple animations is cancelled externally', async () => {
963+
const callback = vi.fn();
964+
let rejectFinished: (error: any) => void;
965+
966+
const cancelledAnimation = createMockAnimation({
967+
finished: new Promise<Animation>((_resolve, reject) => {
968+
rejectFinished = reject;
969+
}),
970+
cancel: vi.fn(() => {
971+
rejectFinished(new DOMException('The animation was aborted.', 'AbortError'));
972+
}),
973+
});
974+
975+
const normalAnimation = createMockAnimation({
976+
finished: new Promise<Animation>(() => {}),
977+
});
978+
979+
const animationGroup = new AnimationGroup([normalAnimation, cancelledAnimation]);
980+
const abortPromise = animationGroup.onAbort(callback);
981+
982+
cancelledAnimation.cancel();
983+
984+
await abortPromise;
985+
986+
expect(callback).toHaveBeenCalledTimes(1);
987+
});
988+
989+
test('should not execute callback for non-AbortError rejections', async () => {
990+
const callback = vi.fn();
991+
992+
const mockAnimation = createMockAnimation({
993+
finished: Promise.reject(new Error('Some other error')),
994+
});
995+
996+
const animationGroup = new AnimationGroup([mockAnimation]);
997+
await animationGroup.onAbort(callback);
998+
999+
expect(callback).not.toHaveBeenCalled();
1000+
});
1001+
1002+
test('should not execute callback when animations finish successfully', async () => {
1003+
const callback = vi.fn();
1004+
1005+
const mockAnimation = createMockAnimation({
1006+
finished: Promise.resolve(undefined as any),
1007+
});
1008+
1009+
const animationGroup = new AnimationGroup([mockAnimation]);
1010+
await animationGroup.onAbort(callback);
1011+
1012+
expect(callback).not.toHaveBeenCalled();
1013+
});
1014+
});
1015+
9321016
describe('playState getter', () => {
9331017
test('should return playState from first animation', () => {
9341018
const mockAnimation1 = createMockAnimation({

0 commit comments

Comments
 (0)