Files
svelte-headlessui/src/lib/utils/transition.ts
2021-12-13 18:22:16 -08:00

98 lines
3.0 KiB
TypeScript

import { once } from "./once";
import { disposables } from "./disposables";
function addClasses(node: HTMLElement, ...classes: string[]) {
node && classes.length > 0 && node.classList.add(...classes);
}
function removeClasses(node: HTMLElement, ...classes: string[]) {
node && classes.length > 0 && node.classList.remove(...classes);
}
export enum Reason {
Finished = "finished",
Cancelled = "cancelled",
}
function waitForTransition(node: HTMLElement, done: (reason: Reason) => void) {
let d = disposables();
if (!node) return d.dispose;
// Safari returns a comma separated list of values, so let's sort them and take the highest value.
let { transitionDuration, transitionDelay } = getComputedStyle(node);
let [durationMs, delaysMs] = [transitionDuration, transitionDelay].map(
(value) => {
let [resolvedValue = 0] = value
.split(",")
// Remove falsy we can't work with
.filter(Boolean)
// Values are returned as `0.3s` or `75ms`
.map((v) => (v.includes("ms") ? parseFloat(v) : parseFloat(v) * 1000))
.sort((a, z) => z - a);
return resolvedValue;
}
);
// Waiting for the transition to end. We could use the `transitionend` event, however when no
// actual transition/duration is defined then the `transitionend` event is not fired.
//
// TODO: Downside is, when you slow down transitions via devtools this timeout is still using the
// full 100% speed instead of the 25% or 10%.
if (durationMs !== 0) {
d.setTimeout(() => {
done(Reason.Finished);
}, durationMs + delaysMs);
} else {
// No transition is happening, so we should cleanup already. Otherwise we have to wait until we
// get disposed.
done(Reason.Finished);
}
// If we get disposed before the timeout runs we should cleanup anyway
d.add(() => done(Reason.Cancelled));
return d.dispose;
}
export function transition(
node: HTMLElement,
base: string[],
from: string[],
to: string[],
entered: string[],
done?: (reason: Reason) => void
) {
let d = disposables();
let _done = done !== undefined ? once(done) : () => {};
removeClasses(node, ...entered);
addClasses(node, ...base, ...from);
d.nextFrame(() => {
removeClasses(node, ...from);
addClasses(node, ...to);
d.add(
waitForTransition(node, (reason) => {
removeClasses(node, ...to, ...base);
addClasses(node, ...entered);
return _done(reason);
})
);
});
// Once we get disposed, we should ensure that we cleanup after ourselves. In case of an unmount,
// the node itself will be nullified and will be a no-op. In case of a full transition the classes
// are already removed which is also a no-op. However if you go from enter -> leave mid-transition
// then we have some leftovers that should be cleaned.
d.add(() => removeClasses(node, ...base, ...from, ...to, ...entered));
// When we get disposed early, than we should also call the done method but switch the reason.
d.add(() => _done(Reason.Cancelled));
return d.dispose;
}