react-smooth-flow
Version:
Effortless React animations for entering, exiting, and updating elements
575 lines (574 loc) • 20.7 kB
JavaScript
import { flushSync as C } from "react-dom";
import { useRef as x, useMemo as W } from "react";
const ht = (t) => ({
"data-transition": JSON.stringify(t)
}), gt = (t) => ({
"data-transitionroot": t
}), g = [
"opacity",
"backgroundColor",
"boxShadow",
"backdropFilter",
"borderRadius",
"borderWidth",
"borderTopWidth",
"borderRightWidth",
"borderBottomWidth",
"borderLeftWidth",
"borderColor",
"borderStyle",
"pointerEvents"
], j = [
"opacity",
"backgroundColor",
"boxShadow",
"borderRadius",
"borderWidth",
"borderColor",
"borderStyle"
], K = [
"duration",
"easing",
"delay",
"ignoreReducedMotion",
"positionAnchor",
"transitionRootTag",
"forcePresenceTransition",
"clip"
], L = ["tag", "transitionRoot"], P = {
debug: !1,
transitionOptions: {
duration: 300,
easing: "ease",
delay: 0,
ignoreReducedMotion: !1,
enterKeyframes: { opacity: [0, 1] },
exitKeyframes: { opacity: [1, 0] },
contentEnterKeyframes: { opacity: [0, 1, 1] },
contentExitKeyframes: { opacity: [1, 1, 0] },
contentAlign: "topLeft",
positionAnchor: "topLeft",
forcePresenceTransition: !1,
transitionRootTag: null,
clip: !0,
relevantStyleProperties: [],
persistBounds: !0,
disabled: !1
}
}, H = (t) => {
const n = t.map(
({ prevSnapshot: o, nextSnapshot: i }) => Math.max((o == null ? void 0 : o.totalZIndex) ?? -1, (i == null ? void 0 : i.totalZIndex) ?? -1)
), e = Math.max(...n);
t.forEach((o) => {
o.transitionType === "mutation" ? o.image.style.zIndex = e.toString() : o.transitionType === "presence" && (o.prevImage && (o.prevImage.style.zIndex = e.toString()), o.nextImage && (o.nextImage.style.zIndex = e.toString()));
});
}, _ = (t, n) => {
t.forEach((e) => {
if (e.transitionType === "mutation" && e.prevSnapshot.transitionOptions.persistBounds) {
const o = Object.keys(e.prevSnapshot.transitionMapping);
for (const i of o) {
const s = n[i];
if (s) {
e.prevSnapshot.bounds = s;
break;
}
}
}
});
}, M = (t) => t.filter(Boolean), m = {}, F = () => {
const t = `${Date.now()}-${Math.random().toString(36)}`;
return m[t] = {}, t;
}, S = (t) => m[t], q = (t) => {
var n;
return (n = Object.entries(m).find(([e, o]) => Object.keys(o).includes(t))) == null ? void 0 : n[1];
}, Z = () => Object.keys(m).flatMap((t) => Object.values(m[t]).flat()), J = (t) => Object.keys(m[t]).flatMap((n) => m[t][n]), V = (...t) => {
const n = M(t), e = Object.entries(m).filter(([s, r]) => Object.keys(r).some((a) => n.includes(a))).map((s) => s[0]);
e.map((s) => m[s]).flatMap((s) => Object.keys(s).flatMap((r) => s[r])).forEach((s) => {
s.animation.cancel(), s.cleanup();
}), e.forEach((s) => delete m[s]);
}, z = (t) => {
Object.keys(m[t]).flatMap((o) => m[t][o]).forEach((o) => o.cleanup()), delete m[t];
}, Y = (t) => Z().some((e) => e.snapshotPair.shared.transitionRoot === t), X = (t) => {
const n = [];
for (const { shared: e } of t)
e.transitionRoot && !n.includes(e.transitionRoot) && n.push(e.transitionRoot);
return n.forEach((e) => {
window.getComputedStyle(e).position === "static" && (e.setAttribute("data-savedposition", e.style.position), e.style.setProperty("position", "relative", "important"));
}), () => {
n.forEach((e) => {
Y(e) || e.dataset.savedposition !== void 0 && (e.style.setProperty("position", e.dataset.savedposition), e.removeAttribute("data-savedposition"));
});
};
}, I = (t, n, e, o) => {
t.style.position = n;
let i = 0, s = 0, r = 0, a = 0;
if (n === "absolute" && !o) {
const d = document.documentElement.scrollWidth - (document.documentElement.scrollLeft + document.documentElement.clientWidth), c = document.documentElement.scrollHeight - (document.documentElement.scrollTop + document.documentElement.clientHeight);
i = window.scrollY, s = d, r = c, a = window.scrollX;
}
switch (e.transitionOptions.positionAnchor) {
case "topLeft":
t.style.top = `${e.bounds.top + i}px`, t.style.left = `${e.bounds.left + a}px`;
break;
case "topRight":
t.style.top = `${e.bounds.top + i}px`, t.style.right = `${e.bounds.right + s}px`;
break;
case "bottomRight":
t.style.right = `${e.bounds.right + s}px`, t.style.bottom = `${e.bounds.bottom + r}px`;
break;
case "bottomLeft":
t.style.bottom = `${e.bounds.bottom + r}px`, t.style.left = `${e.bounds.left + a}px`;
break;
}
}, U = (t) => {
t.forEach((n) => {
if (n.transitionType === "mutation") {
const { prevSnapshot: e, nextSnapshot: o, image: i, shared: s, firstValidSnapshot: r } = n, a = o ?? e, d = a.hasFixedPosition && !a.transitionRoot ? "fixed" : "absolute";
I(i, d, r, s.transitionRoot);
} else if (n.transitionType === "presence") {
const { prevSnapshot: e, nextSnapshot: o, prevImage: i, nextImage: s, shared: r } = n;
[
{ snapshot: e, image: i },
{ snapshot: o, image: s }
].filter(({ snapshot: d, image: c }) => d && c).forEach(({ snapshot: d, image: c }) => {
const l = d.hasFixedPosition && !d.transitionRoot ? "fixed" : "absolute";
I(c, l, d, r.transitionRoot);
});
}
});
}, D = (t) => {
const n = {};
t.forEach((e) => {
if (e.transitionType === "presence")
return;
const { firstValidSnapshot: o, image: i } = e, s = o.targetElement.dataset.transitionroot;
s && (n[s] = i);
}), t.forEach((e) => {
const { shared: o, prevSnapshot: i, nextSnapshot: s } = e;
if (!o.transitionOptions.transitionRootTag || !Object.keys(n).includes(o.transitionOptions.transitionRootTag))
return;
const r = n[o.transitionOptions.transitionRootTag];
i && (i.transitionRoot = r), s && (s.transitionRoot = r), o.transitionRoot = r;
});
}, B = (t, n, e) => {
const o = window.getComputedStyle(t);
e.forEach((r) => {
n.style.setProperty(r, o.getPropertyValue(r), "important");
});
const i = t.children, s = n.children;
for (let r = 0; r < i.length; r++)
B(i[r], s[r], e);
}, G = (t) => {
let n = t;
for (; n && n !== document.body; ) {
if (window.getComputedStyle(n).position === "fixed")
return !0;
n = n.parentElement;
}
return !1;
}, Q = (t) => {
const n = window.getComputedStyle(t);
return Object.fromEntries(
g.map((o) => [o, n[o]])
);
}, w = (t) => {
const n = t.getBoundingClientRect(), e = n.width - t.clientWidth, o = n.height - t.clientHeight;
return {
top: n.top,
right: document.documentElement.clientWidth - n.right,
bottom: document.documentElement.clientHeight - n.bottom,
left: n.left,
width: n.width,
height: n.height,
scrollBarWidth: e,
scrollBarHeight: o
};
}, tt = (t, n = document) => {
const e = n.querySelectorAll(`[data-transitionroot='${t}']`);
if (e.length) {
if (e.length > 1)
throw Error(
`Transition root tag must be unique. Found ${e.length} elements with the "${t}" root tag.`
);
return e[0];
} else return null;
}, v = (t) => {
var n;
if (Array.isArray(t)) {
const e = [...t].reverse();
return e.forEach(
(o) => o.offset = o.offset === void 0 ? void 0 : 1 - o.offset
), e;
} else {
const e = JSON.parse(JSON.stringify(t));
return Object.keys(e).forEach((o) => e[o].reverse()), e.offset = (n = e.offset) == null ? void 0 : n.map((o) => 1 - o), e;
}
}, T = (t) => {
if (!t.dataset.transition)
return null;
const n = JSON.parse(t.dataset.transition);
return Object.keys(n).forEach((e) => {
const o = n[e];
if (o.exitKeyframes === "reversedEnter") {
if (!o.enterKeyframes)
throw Error(
`Transition target with tag "${e}" has "exitKeyframes" property set to "reversedEnter", but "enterKeyframes" was not specified. Either specify "enterKeyframes" or don't set "exitKeyframes" to "reversedEnter"`
);
o.exitKeyframes = v(o.enterKeyframes);
}
if (o.contentExitKeyframes === "reversedEnter") {
if (!o.contentEnterKeyframes)
throw Error(
`Transition target with tag "${e}" has "contentExitKeyframes" property set to "reversedEnter", but "contentEnterKeyframes" was not specified. Either specify "contentEnterKeyframes" or don't set "contentExitKeyframes" to "reversedEnter"`
);
o.contentExitKeyframes = v(o.contentEnterKeyframes);
}
Object.keys(
P.transitionOptions
).forEach((s) => {
if (o[s] === void 0) {
const r = P.transitionOptions[s];
o[s] = r;
}
});
}), n;
}, et = (t, n = document.body) => {
let e = t, o = 1;
for (; e && e !== n; ) {
const i = window.getComputedStyle(e), s = parseFloat(i.opacity);
Number.isNaN(s) || (o *= s), e = e.parentElement;
}
return o;
}, nt = (t, n = document.body) => {
let e = t, o = 0;
for (; e && e !== n; ) {
const i = window.getComputedStyle(e), s = parseInt(i.zIndex);
Number.isNaN(s) || (o += s), e = e.parentElement;
}
return o;
}, E = (t, n = document) => {
const e = n.querySelectorAll("[data-transition]"), o = [];
if (e.forEach((i) => {
const s = T(i);
!s || !Object.keys(s).includes(t) || s[t].disabled || o.push(i);
}), o.length) {
if (o.length > 1)
throw Error(`Found ${o.length} elements with tag "${t}". Transition tag must be unique`);
return o[0];
} else return null;
}, ot = (t, n) => {
t.forEach((e) => {
const o = E(e, n);
o && (o.style.setProperty("opacity", "0", "important"), o.style.setProperty("transition", "none", "important"));
});
}, it = (t, n) => {
const e = T(t);
if (!e)
return !1;
const o = Object.keys(e);
for (const i of o)
if (n.includes(i))
return !0;
return !1;
}, st = (t, n) => {
const e = [];
t.matches("[id]") && e.push(t), e.push(...t.querySelectorAll("[id]"));
const o = {};
e.forEach((i) => {
if (!i.id || it(i, n))
return;
const s = `rsf-${Math.random().toString(16).split(".")[1]}`;
o[i.id] = s, i.id = s;
}), t.querySelectorAll('[clip-path^="url(#"]').forEach((i) => {
var r, a;
const s = (a = (r = i.getAttribute("clip-path")) == null ? void 0 : r.match(/url\(#(.+)\)/)) == null ? void 0 : a[1];
s && i.setAttribute("clip-path", `url(#${o[s]})`);
}), t.querySelectorAll('[mask^="url(#"]').forEach((i) => {
var r, a;
const s = (a = (r = i.getAttribute("mask")) == null ? void 0 : r.match(/url\(#(.+)\)/)) == null ? void 0 : a[1];
s && i.setAttribute("mask", `url(#${o[s]})`);
}), t.querySelectorAll('use[href^="#"]').forEach((i) => {
var r;
const s = (r = i.getAttribute("href")) == null ? void 0 : r.replace("#", "");
s && i.setAttribute("href", `#${o[s]}`);
});
}, $ = (t, n, e) => {
if (!t)
return null;
const o = T(t), i = o[n], s = i.transitionRootTag ? tt(i.transitionRootTag) : null;
if (i.transitionRootTag && !s)
throw Error(`Failed to find transition root with tag "${i.transitionRootTag}"`);
const r = Q(t);
r.opacity = `${et(t, s ?? void 0)}`;
const a = w(t), d = G(t), c = s ? w(s) : null;
c && (a.top -= c.top - s.scrollTop, a.right -= c.right + c.scrollBarWidth + s.scrollLeft, a.left -= c.left - s.scrollLeft, a.bottom -= c.bottom + c.scrollBarHeight + s.scrollTop);
const l = t.cloneNode(!0);
i.relevantStyleProperties.length && B(t, l, i.relevantStyleProperties), ot(e, l), st(l, e), l.style.setProperty("background-color", "transparent", "important"), l.style.setProperty("border-radius", "0", "important"), l.style.setProperty("border-width", "0", "important"), l.style.setProperty("position", "static", "important"), l.style.setProperty("margin", "0", "important"), l.style.setProperty("opacity", "1", "important"), l.style.setProperty("box-shadow", "none", "important"), l.style.setProperty("backdrop-filter", "none", "important");
const p = document.createElement("div");
p.inert = !0, p.classList.add("rsf-snapshotContainer", `rsf-${i.contentAlign}`), p.style.setProperty("--borderTopWidth", r.borderTopWidth), p.style.setProperty("--borderRightWidth", r.borderRightWidth), p.style.setProperty("--borderBottomWidth", r.borderBottomWidth), p.style.setProperty("--borderLeftWidth", r.borderLeftWidth);
const f = document.createElement("div");
return f.className = "rsf-transformContainer", f.style.width = `${a.width}px`, f.style.height = `${a.height}px`, f.innerHTML = l.outerHTML.replace(/\sdata-transition=".+?"/gm, "").replace(/\sdata-transitionroot=".+?"/gm, ""), p.append(f), {
tag: n,
bounds: a,
image: p,
computedStyle: r,
transitionOptions: i,
transitionMapping: o,
hasFixedPosition: d,
transitionRoot: s,
targetElement: t,
totalZIndex: nt(t, s ?? void 0)
};
}, rt = (t) => {
const n = [];
return t.forEach((e) => {
const o = E(e);
if (!o) {
n.push(e);
return;
}
const i = T(o);
if (!i) {
n.push(e);
return;
}
const s = Object.keys(i);
n.push(...s);
}), n;
}, at = (t) => {
const n = q(t);
if (!n)
return null;
const e = n[t][0].snapshotPair;
return e.transitionType === "presence" ? null : w(e.image);
}, ct = () => window.matchMedia("(prefers-reduced-motion: reduce)").matches, O = (t, n) => {
const e = document.createElement("div");
return e.className = "rsf-image", e.style.overflow = t ? "hidden" : "visible", e.style.width = `${n.width}px`, e.style.height = `${n.height}px`, e;
}, lt = (t, n) => t.map((i, s) => ({ prevSnapshot: i, nextSnapshot: n[s] })).filter(
(i) => {
var s, r;
return ((s = i.prevSnapshot) == null ? void 0 : s.transitionOptions.ignoreReducedMotion) || ((r = i.nextSnapshot) == null ? void 0 : r.transitionOptions.ignoreReducedMotion) || !ct();
}
).map(({ prevSnapshot: i, nextSnapshot: s }) => {
const r = i ?? s;
if (!r)
return null;
const a = Object.fromEntries(
K.map((c) => [c, r.transitionOptions[c]])
), d = {
tag: r.tag,
transitionRoot: r.transitionRoot ?? null,
transitionOptions: a
};
if (i && s && !d.transitionOptions.forcePresenceTransition) {
const c = r.computedStyle, l = O(d.transitionOptions.clip, r.bounds);
return g.forEach((p) => l.style[p] = c[p]), i && l.append(i.image), s && l.append(s.image), {
prevSnapshot: i,
nextSnapshot: s,
firstValidSnapshot: r,
image: l,
shared: d,
transitionType: "mutation"
};
} else {
let c = null, l = null;
return i && (c = O(d.transitionOptions.clip, i.bounds), g.forEach(
(p) => c.style[p] = i.computedStyle[p]
), c.append(i.image)), s && (l = O(d.transitionOptions.clip, s.bounds), g.forEach(
(p) => l.style[p] = s.computedStyle[p]
), l.append(s.image)), {
prevSnapshot: i,
nextSnapshot: s,
firstValidSnapshot: r,
prevImage: c,
nextImage: l,
shared: d,
transitionType: "presence"
};
}
}).filter(Boolean), b = (t) => {
if (Array.isArray(t)) {
const n = t.slice(0, 1);
return n.length ? (delete n[0].offset, n) : [];
} else {
const n = JSON.parse(JSON.stringify(t));
return delete n.offset, Object.keys(n).forEach((e) => n[e].splice(1)), n;
}
}, k = () => {
const t = "transitionRoot";
let n = document.getElementById(t);
return n || (n = document.createElement("div"), n.id = t, n.ariaHidden = "true", document.body.append(n), document.documentElement.style.position = "relative"), n;
}, N = (t) => {
const n = {
opacity: t.style.opacity,
transition: t.style.transition,
pointerEvents: t.style.pointerEvents,
ariaHidden: t.ariaHidden
};
return t.style.setProperty("opacity", "0", "important"), t.style.setProperty("transition", "none", "important"), t.style.setProperty("pointer-events", "none", "important"), t.ariaHidden = "true", () => {
t.style.opacity = n.opacity, t.style.pointerEvents = n.pointerEvents, t.ariaHidden = n.ariaHidden, setTimeout(() => t.style.transition = n.transition);
};
}, dt = ({ prevSnapshot: t, nextSnapshot: n, shared: e }) => {
const o = [t, n].map((l) => {
const p = {
width: `${l.bounds.width}px`,
height: `${l.bounds.height}px`
};
return j.forEach((f) => p[f] = l.computedStyle[f]), p;
});
let i = "";
const s = n.bounds.top - t.bounds.top, r = t.bounds.right - n.bounds.right, a = t.bounds.bottom - n.bounds.bottom, d = n.bounds.left - t.bounds.left;
switch (e.transitionOptions.positionAnchor) {
case "topLeft":
i = `translate(${d}px, ${s}px)`;
break;
case "topRight":
i = `translate(${r}px, ${s}px)`;
break;
case "bottomRight":
i = `translate(${r}px, ${a}px)`;
break;
case "bottomLeft":
i = `translate(${d}px, ${a}px)`;
break;
}
return [
{ ...o[0], transform: "translate(0, 0)" },
{ ...o[1], transform: i }
];
}, pt = (t, n) => {
const { shared: e, prevSnapshot: o, nextSnapshot: i, image: s } = t, r = e.transitionRoot ?? k(), a = N(i.targetElement);
n[e.tag] = [], r.append(s);
const d = {
duration: e.transitionOptions.duration,
easing: e.transitionOptions.easing,
delay: e.transitionOptions.delay,
fill: "forwards"
}, c = dt(t);
e.transitionOptions.delay && s.animate(b(c), { fill: "forwards" });
const l = () => {
s.remove(), a();
}, p = s.animate(c, d);
n[e.tag].push({
animation: p,
snapshotPair: t,
cleanup: l
});
const f = s.children[0], y = s.children[1];
e.transitionOptions.delay && (f.animate(b(o.transitionOptions.contentExitKeyframes), {
fill: "forwards"
}), y.animate(b(i.transitionOptions.contentEnterKeyframes), {
fill: "forwards"
}));
const h = f.animate(
o.transitionOptions.contentExitKeyframes,
d
), u = y.animate(
i.transitionOptions.contentEnterKeyframes,
d
);
n[e.tag].push(
{ animation: h, snapshotPair: t, cleanup: l },
{ animation: u, snapshotPair: t, cleanup: l }
);
}, A = (t, n, e, o, i, s) => {
const { shared: r } = o;
n.append(t), r.transitionOptions.delay && t.animate(b(e), {
fill: "forwards"
});
const a = t.animate(e, {
duration: r.transitionOptions.duration,
easing: r.transitionOptions.easing,
delay: r.transitionOptions.delay,
fill: "forwards"
});
return i[r.tag].push({
snapshotPair: o,
animation: a,
cleanup: () => {
t.remove(), s == null || s();
}
}), a.finished;
}, ut = (t, n) => {
const { shared: e, prevSnapshot: o, nextSnapshot: i, prevImage: s, nextImage: r } = t, a = e.transitionRoot ?? k(), d = i != null && i.targetElement ? N(i.targetElement) : null;
n[e.tag] = [], o && s && A(
s,
a,
o.transitionOptions.exitKeyframes,
t,
n,
d
), i && r && A(
r,
a,
i.transitionOptions.enterKeyframes,
t,
n,
d
);
}, ft = (t, n) => {
t.forEach(({ prevSnapshot: e, nextSnapshot: o }) => {
e && o && (K.forEach((i) => {
if (e.transitionOptions[i] !== o.transitionOptions[i])
throw Error(
`"${i}" transition property differ for previous and next snapshots. It should never update while snapshots are being captured. Transition tag: ${e.tag}`
);
}), L.forEach((i) => {
if (e[i] !== o[i])
throw Error(
`"${i}" snapshot property differ for previous and next snapshots. It should never update while snapshots are being captured. Transition tag: ${e.tag}`
);
}));
});
}, bt = async (t, n, e) => {
var f, y, h;
const o = { flushSync: !0, ...e }, i = M(t), s = rt(i), r = Object.fromEntries(
s.map((u) => [u, at(u)])
);
V(...s);
const a = i.map(
(u) => $(
E(u),
u,
i.filter((R) => R !== u)
)
);
n ? o.flushSync ? C(n) : n() : await void 0;
const d = i.map(
(u) => $(
E(u),
u,
i.filter((R) => R !== u)
)
), c = lt(a, d);
ft(c), D(c), _(c, r), U(c), H(c);
const l = X(c);
(f = o.onBegin) == null || f.call(o);
const p = F();
try {
for (const u of c)
u.transitionType === "mutation" ? pt(u, S(p)) : u.transitionType === "presence" && ut(u, S(p));
if (P.debug)
return;
await Promise.all(J(p).map((u) => u.animation.finished)), z(p), l(), (y = o.onFinish) == null || y.call(o);
} catch (u) {
if (u.name === "AbortError") {
l(), (h = o.onCancel) == null || h.call(o);
return;
}
throw u;
}
}, Et = (t, n) => {
const e = x(!0), o = x(null);
W(() => {
o.current && o.current(), o.current = t(e.current), e.current && (e.current = !1);
}, n ?? [{}]);
};
export {
V as cancelTransition,
ht as constructTransition,
gt as constructTransitionRoot,
P as defaults,
bt as startTransition,
Et as usePreCommitEffect
};