UNPKG

3.84 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 function isTransitionEvent(event: Event): event is TransitionEvent {
30 return 'propertyName' in event;
31 }
32
33 let onTransitionStart = (e: Event) => {
34 if (!isTransitionEvent(e) || !e.target) {
35 return;
36 }
37 // Add the transitioning property to the list for this element.
38 let transitions = transitionsByElement.get(e.target);
39 if (!transitions) {
40 transitions = new Set();
41 transitionsByElement.set(e.target, transitions);
42
43 // The transitioncancel event must be registered on the element itself, rather than as a global
44 // event. This enables us to handle when the node is deleted from the document while it is transitioning.
45 // In that case, the cancel event would have nowhere to bubble to so we need to handle it directly.
46 e.target.addEventListener('transitioncancel', onTransitionEnd, {
47 once: true
48 });
49 }
50
51 transitions.add(e.propertyName);
52 };
53
54 let onTransitionEnd = (e: Event) => {
55 if (!isTransitionEvent(e) || !e.target) {
56 return;
57 }
58 // Remove property from list of transitioning properties.
59 let properties = transitionsByElement.get(e.target);
60 if (!properties) {
61 return;
62 }
63
64 properties.delete(e.propertyName);
65
66 // If empty, remove transitioncancel event, and remove the element from the list of transitioning elements.
67 if (properties.size === 0) {
68 e.target.removeEventListener('transitioncancel', onTransitionEnd);
69 transitionsByElement.delete(e.target);
70 }
71
72 // If no transitioning elements, call all of the queued callbacks.
73 if (transitionsByElement.size === 0) {
74 for (let cb of transitionCallbacks) {
75 cb();
76 }
77
78 transitionCallbacks.clear();
79 }
80 };
81
82 document.body.addEventListener('transitionrun', onTransitionStart);
83 document.body.addEventListener('transitionend', onTransitionEnd);
84}
85
86if (typeof document !== 'undefined') {
87 if (document.readyState !== 'loading') {
88 setupGlobalEvents();
89 } else {
90 document.addEventListener('DOMContentLoaded', setupGlobalEvents);
91 }
92}
93
94export function runAfterTransition(fn: () => void) {
95 // Wait one frame to see if an animation starts, e.g. a transition on mount.
96 requestAnimationFrame(() => {
97 // If no transitions are running, call the function immediately.
98 // Otherwise, add it to a list of callbacks to run at the end of the animation.
99 if (transitionsByElement.size === 0) {
100 fn();
101 } else {
102 transitionCallbacks.add(fn);
103 }
104 });
105}