Skip to content

Commit 94339e5

Browse files
authored
Handle error cases in element.animate() (#34096)
1 parent 8f84c46 commit 94339e5

File tree

3 files changed

+92
-26
lines changed

3 files changed

+92
-26
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "fix: handle error cases in \"element.animate()\"",
4+
"packageName": "@fluentui/react-motion",
5+
"email": "seanmonahan@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.test.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@ function createElementMock() {
1111
return [{ animate } as unknown as HTMLElement, animate] as const;
1212
}
1313

14+
function createNullElementMock() {
15+
const animate = jest.fn().mockReturnValue(null);
16+
17+
return [{ animate } as unknown as HTMLElement, animate] as const;
18+
}
19+
20+
function createErrorElementMock() {
21+
const animate = jest.fn().mockImplementation(() => {
22+
throw new Error('Animation error');
23+
});
24+
25+
return [{ animate } as unknown as HTMLElement, animate] as const;
26+
}
27+
1428
const DEFAULT_KEYFRAMES = [{ transform: 'rotate(0)' }, { transform: 'rotate(180deg)' }];
1529
const REDUCED_MOTION_KEYFRAMES = [{ opacity: 0 }, { opacity: 1 }];
1630

@@ -85,4 +99,39 @@ describe('useAnimateAtoms', () => {
8599
});
86100
});
87101
});
102+
103+
// See: https://github.com/microsoft/fluentui/issues/33902
104+
describe('error handling', () => {
105+
it('handle "element.animate()" returning null', () => {
106+
const { result } = renderHook(() => useAnimateAtoms());
107+
108+
const [element, animateMock] = createNullElementMock();
109+
const motion: AtomMotion = {
110+
keyframes: DEFAULT_KEYFRAMES,
111+
reducedMotion: { duration: 100, easing: 'linear' },
112+
};
113+
114+
const handle = result.current(element, motion, { isReducedMotion: false });
115+
116+
expect(animateMock).toHaveBeenCalledTimes(1);
117+
expect(animateMock).toHaveReturnedWith(null);
118+
expect(handle).toBeDefined();
119+
});
120+
121+
it('handles "element.animate()" throwing an error', () => {
122+
const { result } = renderHook(() => useAnimateAtoms());
123+
124+
const [element, animateMock] = createErrorElementMock();
125+
const motion: AtomMotion = {
126+
keyframes: DEFAULT_KEYFRAMES,
127+
reducedMotion: { duration: 100, easing: 'linear' },
128+
};
129+
130+
const handle = result.current(element, motion, { isReducedMotion: false });
131+
132+
expect(animateMock).toHaveBeenCalledTimes(1);
133+
expect(animateMock).toThrow();
134+
expect(handle).toBeDefined();
135+
});
136+
});
88137
});

packages/react-components/react-motion/library/src/hooks/useAnimateAtoms.ts

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,42 @@ function useAnimateAtomsInSupportedEnvironment() {
2828
const atoms = Array.isArray(value) ? value : [value];
2929
const { isReducedMotion } = options;
3030

31-
const animations = atoms.map(motion => {
32-
// Grab the custom reduced motion definition if it exists, or fall back to the default reduced motion.
33-
const { keyframes: motionKeyframes, reducedMotion = DEFAULT_REDUCED_MOTION_ATOM, ...params } = motion;
34-
// Grab the reduced motion keyframes if they exist, or fall back to the regular keyframes.
35-
const { keyframes: reducedMotionKeyframes = motionKeyframes, ...reducedMotionParams } = reducedMotion;
36-
37-
const animationKeyframes: Keyframe[] = isReducedMotion ? reducedMotionKeyframes : motionKeyframes;
38-
const animationParams: KeyframeEffectOptions = {
39-
...DEFAULT_ANIMATION_OPTIONS,
40-
...params,
41-
42-
// Use reduced motion overrides (e.g. duration, easing) when reduced motion is enabled
43-
...(isReducedMotion && reducedMotionParams),
44-
};
45-
46-
const animation = element.animate(animationKeyframes, animationParams);
47-
48-
if (SUPPORTS_PERSIST) {
49-
animation.persist();
50-
} else {
51-
const resultKeyframe = animationKeyframes[animationKeyframes.length - 1];
52-
Object.assign(element.style ?? {}, resultKeyframe);
53-
}
54-
55-
return animation;
56-
});
31+
const animations = atoms
32+
.map(motion => {
33+
// Grab the custom reduced motion definition if it exists, or fall back to the default reduced motion.
34+
const { keyframes: motionKeyframes, reducedMotion = DEFAULT_REDUCED_MOTION_ATOM, ...params } = motion;
35+
// Grab the reduced motion keyframes if they exist, or fall back to the regular keyframes.
36+
const { keyframes: reducedMotionKeyframes = motionKeyframes, ...reducedMotionParams } = reducedMotion;
37+
38+
const animationKeyframes: Keyframe[] = isReducedMotion ? reducedMotionKeyframes : motionKeyframes;
39+
const animationParams: KeyframeEffectOptions = {
40+
...DEFAULT_ANIMATION_OPTIONS,
41+
...params,
42+
43+
// Use reduced motion overrides (e.g. duration, easing) when reduced motion is enabled
44+
...(isReducedMotion && reducedMotionParams),
45+
};
46+
47+
try {
48+
// Firefox can throw an error when calling `element.animate()`.
49+
// See: https://github.com/microsoft/fluentui/issues/33902
50+
const animation = element.animate(animationKeyframes, animationParams);
51+
52+
if (SUPPORTS_PERSIST) {
53+
// Chromium browsers can return null when calling `element.animate()`.
54+
// See: https://github.com/microsoft/fluentui/issues/33902
55+
animation?.persist();
56+
} else {
57+
const resultKeyframe = animationKeyframes[animationKeyframes.length - 1];
58+
Object.assign(element.style ?? {}, resultKeyframe);
59+
}
60+
61+
return animation;
62+
} catch (e) {
63+
return null;
64+
}
65+
})
66+
.filter(animation => !!animation) as Animation[];
5767

5868
return {
5969
set playbackRate(rate: number) {

0 commit comments

Comments
 (0)