UNPKG

3.56 kBPlain TextView Raw
1/*
2 * Copyright 2020 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12
13import {flushSync} from 'react-dom';
14import {RefObject, useCallback, useState} from 'react';
15import {useLayoutEffect} from './useLayoutEffect';
16
17export function useEnterAnimation(ref: RefObject<HTMLElement | null>, isReady: boolean = true) {
18 let [isEntering, setEntering] = useState(true);
19 let isAnimationReady = isEntering && isReady;
20
21 // There are two cases for entry animations:
22 // 1. CSS @keyframes. The `animation` property is set during the isEntering state, and it is removed after the animation finishes.
23 // 2. CSS transitions. The initial styles are applied during the isEntering state, and removed immediately, causing the transition to occur.
24 //
25 // In the second case, cancel any transitions that were triggered prior to the isEntering = false state (when the transition is supposed to start).
26 // This can happen when isReady starts as false (e.g. popovers prior to placement calculation).
27 useLayoutEffect(() => {
28 if (isAnimationReady && ref.current && 'getAnimations' in ref.current) {
29 for (let animation of ref.current.getAnimations()) {
30 if (animation instanceof CSSTransition) {
31 animation.cancel();
32 }
33 }
34 }
35 }, [ref, isAnimationReady]);
36
37 useAnimation(ref, isAnimationReady, useCallback(() => setEntering(false), []));
38 return isAnimationReady;
39}
40
41export function useExitAnimation(ref: RefObject<HTMLElement | null>, isOpen: boolean) {
42 let [exitState, setExitState] = useState<'closed' | 'open' | 'exiting'>(isOpen ? 'open' : 'closed');
43
44 switch (exitState) {
45 case 'open':
46 // If isOpen becomes false, set the state to exiting.
47 if (!isOpen) {
48 setExitState('exiting');
49 }
50 break;
51 case 'closed':
52 case 'exiting':
53 // If we are exiting and isOpen becomes true, the animation was interrupted.
54 // Reset the state to open.
55 if (isOpen) {
56 setExitState('open');
57 }
58 break;
59 }
60
61 let isExiting = exitState === 'exiting';
62 useAnimation(
63 ref,
64 isExiting,
65 useCallback(() => {
66 // Set the state to closed, which will cause the element to be unmounted.
67 setExitState(state => state === 'exiting' ? 'closed' : state);
68 }, [])
69 );
70
71 return isExiting;
72}
73
74function useAnimation(ref: RefObject<HTMLElement | null>, isActive: boolean, onEnd: () => void) {
75 useLayoutEffect(() => {
76 if (isActive && ref.current) {
77 if (!('getAnimations' in ref.current)) {
78 // JSDOM
79 onEnd();
80 return;
81 }
82
83 let animations = ref.current.getAnimations();
84 if (animations.length === 0) {
85 onEnd();
86 return;
87 }
88
89 let canceled = false;
90 Promise.all(animations.map(a => a.finished)).then(() => {
91 if (!canceled) {
92 flushSync(() => {
93 onEnd();
94 });
95 }
96 }).catch(() => {});
97
98 return () => {
99 canceled = true;
100 };
101 }
102 }, [ref, isActive, onEnd]);
103}