react-sticky-kit
Version:
A lightweight, flexible React sticky container and item component library supporting multiple sticky modes, advanced layouts, and edge cases.
158 lines (157 loc) • 5.57 kB
JavaScript
import { jsx as w } from "react/jsx-runtime";
import { createContext as Z, useContext as L, useRef as k, useState as T, useEffect as E, useCallback as v, useMemo as _ } from "react";
const O = 20, A = 200, $ = Z(null);
function z() {
return L($);
}
function M({ mode: f, children: y, className: h, ...I }) {
const e = z(), g = k(null), S = k(null), [m, o] = T(!1), l = k({
fixedOffsetTop: (e == null ? void 0 : e.fixedOffsetTop) || 0,
isSticky: m,
baseZIndex: (e == null ? void 0 : e.baseZIndex) || O
});
return l.current.baseZIndex = (e == null ? void 0 : e.baseZIndex) || O, l.current.fixedOffsetTop = (e == null ? void 0 : e.fixedOffsetTop) || 0, l.current.isSticky = m, E(() => {
const t = S.current, a = g.current, n = f || (e == null ? void 0 : e.mode);
if (!e || !a || !t || n === "none" || !n || !e.register) {
o(!1);
return;
}
const x = {
el: a,
update: (B, c, s, r, u) => {
if (!B || c > s)
return l.current.isSticky && o(!1), 0;
const i = t.getBoundingClientRect().height;
let p = s;
if (n === "replace" && typeof r < "u") {
const d = r - (s + i);
if (d < 0 && (p = s + d, d + i < 0))
return l.current.isSticky && o(!1), 0;
}
return a.style.height = `${i}px`, l.current.isSticky || o(!0), t.style.top = `${p}px`, t.style.width = `${a.offsetWidth}px`, t.style.zIndex = `${l.current.baseZIndex + (n === "replace" ? -u : u)}`, n === "replace" ? 0 : i;
}
};
return e.register(x);
}, [e == null ? void 0 : e.mode, e == null ? void 0 : e.register, f]), E(() => {
const t = g.current, a = S.current, n = e == null ? void 0 : e.updateStickyItemsHeight;
if (!m || !t || !a || !n)
return;
const H = n(a.getBoundingClientRect().height);
return () => {
N(t, ["height"]), N(a, ["top", "z-index", "width"]), H();
};
}, [m, e == null ? void 0 : e.updateStickyItemsHeight]), /* @__PURE__ */ w("div", { className: "oe-sticky-item " + (h || ""), ...I, ref: g, children: /* @__PURE__ */ w("div", { className: "oe-sticky-content " + (m ? "is-sticky" : ""), ref: S, children: y }) });
}
function N(f, y) {
y.forEach((h) => {
f.style.removeProperty(h);
});
}
function P({
children: f,
offsetTop: y = 0,
baseZIndex: h,
onStickyItemsHeightChange: I,
defaultMode: e = "replace",
constraint: g,
...S
}) {
const m = k(null), o = k([]), l = k(null), t = k({
fixedOffsetTop: y,
defaultMode: e,
stickyItemsHeight: 0,
onStickyItemsHeightChange: I,
lastCanSticky: !1
});
t.current.onStickyItemsHeightChange = I;
const a = v(() => {
var c;
return g === "none" ? {
top: -1 / 0,
bottom: 1 / 0,
left: -1 / 0,
right: 1 / 0,
width: 1 / 0,
height: 1 / 0,
x: 0,
y: 0,
toJSON: () => ({})
} : ((c = m.current) == null ? void 0 : c.getBoundingClientRect()) || { top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0, x: 0, y: 0, toJSON: () => ({}) };
}, [g]), n = v(() => {
const c = m.current;
!c || l.current || (l.current = requestAnimationFrame(() => {
l.current = null;
const s = a(), { fixedOffsetTop: r, stickyItemsHeight: u } = t.current;
let i;
if (g === "none" ? i = !0 : i = !(s.top > r || s.bottom < r), !i) {
t.current.lastCanSticky !== i && (c.classList.toggle("can-sticky", !1), o.current.forEach((C) => C.update(!1, 0, 0, 0, 0)), t.current.lastCanSticky = i);
return;
}
t.current.lastCanSticky = i, u > 0 && c.classList.toggle("can-sticky", !0);
let p, d = 0;
p = r, d = s.bottom - (r + u), d > 0 && (d = 0);
const b = o.current.map((C) => C.el.getBoundingClientRect().top);
o.current.forEach((C, R) => {
p += C.update(i, b[R], p + d, b[R + 1], R);
});
}));
}, [a, g]), H = v((c) => {
const s = t.current.stickyItemsHeight + c;
return t.current.stickyItemsHeight = s, setTimeout(() => {
var r, u;
(u = (r = t.current).onStickyItemsHeightChange) == null || u.call(r, s);
}, 0), () => {
const r = t.current.stickyItemsHeight - c;
t.current.stickyItemsHeight = r, setTimeout(() => {
var u, i;
(i = (u = t.current).onStickyItemsHeightChange) == null || i.call(u, r);
}, 0);
};
}, []);
E(() => {
t.current.fixedOffsetTop = y, t.current.defaultMode = e, setTimeout(() => {
n();
}, 10);
}, [y, e, n]);
const x = v((c) => (o.current.push(c), o.current = W(o.current), n(), () => {
o.current = o.current.filter((s) => s !== c), n();
}), [n]);
E(() => (window.addEventListener("scroll", n, { passive: !0 }), window.addEventListener("resize", n, { passive: !0 }), () => {
window.removeEventListener("scroll", n), window.removeEventListener("resize", n);
}), [n]);
const B = _(() => D(h), [h]);
return /* @__PURE__ */ w(
$.Provider,
{
value: {
register: x,
baseZIndex: B,
updateStickyItemsHeight: H,
fixedOffsetTop: y,
mode: e
},
children: /* @__PURE__ */ w(
"div",
{
...S,
ref: m,
className: "oe-sticky-container",
children: f
}
)
}
);
}
function D(f) {
return typeof f > "u" ? A : Math.max(Number(f) || 0, O);
}
function W(f) {
return f.sort((y, h) => {
const I = y.el.getBoundingClientRect(), e = h.el.getBoundingClientRect();
return I.top - e.top;
});
}
export {
P as StickyContainer,
M as StickyItem
};