UNPKG

3.59 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
13// We store a global list of elements that are currently transitioning,
14// mapped to a set of CSS properties that are transitioning for that element.
15// This is necessary rather than a simple count of transitions because of browser
16// bugs, e.g. Chrome sometimes fires both transitionend and transitioncancel rather
17// than one or the other. So we need to track what's actually transitioning so that
18// we can ignore these duplicate events.
19let transitionsByElement = new Map<EventTarget, Set<string>>();
20
21// A list of callbacks to call once there are no transitioning elements.
22let transitionCallbacks = new Set<() => void>();
23
24function setupGlobalEvents() {
25 if (typeof window === 'undefined') {
26 return;
27 }
28
29 let onTransitionStart = (e: TransitionEvent) => {
30 // Add the transitioning property to the list for this element.
31 let transitions = transitionsByElement.get(e.target);
32 if (!transitions) {
33 transitions = new Set();
34 transitionsByElement.set(e.target, transitions);
35
36 // The transitioncancel event must be registered on the element itself, rather than as a global
37 // event. This enables us to handle when the node is deleted from the document while it is transitioning.
38 // In that case, the cancel event would have nowhere to bubble to so we need to handle it directly.
39 e.target.addEventListener('transitioncancel', onTransitionEnd);
40 }
41
42 transitions.add(e.propertyName);
43 };
44
45 let onTransitionEnd = (e: TransitionEvent) => {
46 // Remove property from list of transitioning properties.
47 let properties = transitionsByElement.get(e.target);
48 if (!properties) {
49 return;
50 }
51
52 properties.delete(e.propertyName);
53
54 // If empty, remove transitioncancel event, and remove the element from the list of transitioning elements.
55 if (properties.size === 0) {
56 e.target.removeEventListener('transitioncancel', onTransitionEnd);
57 transitionsByElement.delete(e.target);
58 }
59
60 // If no transitioning elements, call all of the queued callbacks.
61 if (transitionsByElement.size === 0) {
62 for (let cb of transitionCallbacks) {
63 cb();
64 }
65
66 transitionCallbacks.clear();
67 }
68 };
69
70 document.body.addEventListener('transitionrun', onTransitionStart);
71 document.body.addEventListener('transitionend', onTransitionEnd);
72}
73
74if (typeof document !== 'undefined') {
75 if (document.readyState !== 'loading') {
76 setupGlobalEvents();
77 } else {
78 document.addEventListener('DOMContentLoaded', setupGlobalEvents);
79 }
80}
81
82export function runAfterTransition(fn: () => void) {
83 // Wait one frame to see if an animation starts, e.g. a transition on mount.
84 requestAnimationFrame(() => {
85 // If no transitions are running, call the function immediately.
86 // Otherwise, add it to a list of callbacks to run at the end of the animation.
87 if (transitionsByElement.size === 0) {
88 fn();
89 } else {
90 transitionCallbacks.add(fn);
91 }
92 });
93}