UNPKG

react-smooth-flow

Version:

Effortless React animations for entering, exiting, and updating elements

575 lines (574 loc) 20.7 kB
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 };