react-sticky-kit 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,15 +4,24 @@
4
4
  <a href="https://www.npmjs.com/package/react-sticky-kit" target="_blank">
5
5
  <img src="https://img.shields.io/npm/v/react-sticky-kit.svg?style=flat-square" alt="NPM Version" />
6
6
  </a>
7
+ <a href="https://img.shields.io/npm/dm/react-sticky-kit?style=flat-square" target="_blank">
8
+ <img src="https://img.shields.io/npm/dm/react-sticky-kit?style=flat-square" alt="NPM Downloads" />
9
+ </a>
7
10
  <a href="https://github.com/oe/react-sticky-kit" target="_blank">
8
11
  <img src="https://img.shields.io/github/stars/oe/react-sticky-kit?style=flat-square" alt="GitHub Stars" />
9
12
  </a>
10
13
  <a href="https://github.com/oe/react-sticky-kit/actions" target="_blank">
11
- <img src="https://github.com/oe/react-sticky-kit/actions/workflows/ci.yml/badge.svg" alt="Build Status" />
14
+ <img src="https://img.shields.io/github/actions/workflow/status/oe/react-sticky-kit/ci.yml?style=flat-square" alt="Build Status" />
12
15
  </a>
13
16
  <a href="https://github.com/oe/react-sticky-kit/blob/main/LICENSE" target="_blank">
14
17
  <img src="https://img.shields.io/github/license/oe/react-sticky-kit?style=flat-square" alt="License" />
15
18
  </a>
19
+ <a href="https://www.typescriptlang.org/" target="_blank">
20
+ <img src="https://img.shields.io/badge/TypeScript-5.0%2B-blue?logo=typescript&style=flat-square" alt="TypeScript" />
21
+ </a>
22
+ <a href="#ssr-ssg-support" target="_blank">
23
+ <img src="https://img.shields.io/badge/SSR%2FSSG-Compatible-brightgreen?style=flat-square" alt="SSR/SSG Compatible" />
24
+ </a>
16
25
  </p>
17
26
 
18
27
  A lightweight, flexible React sticky container and item component library. Easily create sticky headers, sections, and advanced sticky layouts with support for multiple modes and edge cases.
@@ -22,6 +31,7 @@ A lightweight, flexible React sticky container and item component library. Easil
22
31
  - 📦 Simple API: `<StickyContainer>` and `<StickyItem>`
23
32
  - 🧩 Supports `replace`, `stack`, and `none` sticky modes
24
33
  - 🏷️ Customizable offset, z-index (baseZIndex), and sticky logic
34
+ - 🔄 Supports SSR/SSG (Next.js, Gatsby, Astro, etc.)
25
35
  - 🧪 Handles edge cases: empty sections, dynamic heights, zero-height headers, long headers, etc.
26
36
  - ⚡️ Written in TypeScript, fully typed
27
37
  - 🧪 Includes demo pages for real-world scenarios
@@ -36,6 +46,9 @@ yarn add react-sticky-kit
36
46
  pnpm add react-sticky-kit
37
47
  ```
38
48
 
49
+ ## Demo
50
+ - [Apple iOS Contact App](https://codesandbox.io/p/sandbox/dreamy-hofstadter-v9dzfz)
51
+
39
52
  ## Usage
40
53
 
41
54
  ```tsx
@@ -74,7 +87,7 @@ export default function Example() {
74
87
  ### `<StickyItem />`
75
88
  | Prop | Type | Default | Description |
76
89
  |---------|--------------------------------------|---------|---------------------------------------------|
77
- | `mode` | `'replace' \| 'stack' \| 'none'` | | Sticky mode for this item (overrides group) |
90
+ | `mode` | `'replace' \| 'stack' \| 'none'` | | Sticky mode for this item (overrides StickyContainer) |
78
91
 
79
92
  ## Sticky Modes
80
93
  - **replace**: Only one sticky item is visible at a time, replacing the previous.
@@ -90,7 +103,57 @@ pnpm install
90
103
  pnpm dev
91
104
  ```
92
105
 
93
- Open [http://localhost:5173](http://localhost:5173) and switch between demo pages to explore all features and edge cases.
106
+ Open [http://localhost:5173](http://localhost:5173) and switch between demo pages to explore all features and edge cases:
107
+ * [iOS Contact](http://localhost:5173/#ios-contact) a dead simple iOS contact clone
108
+ * [Mixed mode](http://localhost:5173/#mixed-mode) mix replace/stack/none mode
109
+ * [Nested](http://localhost:5173/#dynamic-height) nest sticky containers
110
+ * [Dynamic sticky items](http://localhost:5173/#nested) dynamic sticky items(add/remove)
111
+ * [Dynamic offsetTop](http://localhost:5173/#dynamic-offset) dynamic offsetTop that adapts to header height changes
112
+
113
+ ## SSR/SSG Support
114
+
115
+ React Sticky Kit supports Server-Side Rendering (SSR) and Static Site Generation (SSG), working seamlessly with frameworks like Next.js, Gatsby, Astro, and more.
116
+
117
+ ### Next.js Example
118
+
119
+ ```tsx
120
+ // pages/index.tsx
121
+ import { StickyContainer, StickyItem } from 'react-sticky-kit'
122
+ import 'react-sticky-kit/dist/style.css'
123
+
124
+ export default function Home() {
125
+ return (
126
+ <StickyContainer>
127
+ <StickyItem>
128
+ <header>Sticky Header</header>
129
+ </StickyItem>
130
+ <main>Content...</main>
131
+ </StickyContainer>
132
+ )
133
+ }
134
+ ```
135
+
136
+ ### Astro Example
137
+
138
+ ```astro
139
+ ---
140
+ // src/pages/index.astro
141
+ import { StickyContainer, StickyItem } from 'react-sticky-kit'
142
+ import 'react-sticky-kit/dist/style.css'
143
+ ---
144
+
145
+ <html>
146
+ <head>...</head>
147
+ <body>
148
+ <StickyContainer client:load>
149
+ <StickyItem>
150
+ <header>Sticky Header</header>
151
+ </StickyItem>
152
+ <div>Content...</div>
153
+ </StickyContainer>
154
+ </body>
155
+ </html>
156
+ ```
94
157
 
95
158
  ## License
96
159
 
package/dist/index.d.ts CHANGED
@@ -30,7 +30,7 @@ export declare interface IStickyContainerProps extends default_2.HTMLAttributes<
30
30
  export declare interface IStickyItemProps extends default_2.HTMLAttributes<HTMLDivElement> {
31
31
  children: default_2.ReactNode;
32
32
  /**
33
- * Sticky mode for this item. Defaults to the group's mode if not specified.
33
+ * Sticky mode for this item. Defaults to the StickyContainer's mode if not specified.
34
34
  */
35
35
  mode?: IStickyMode;
36
36
  }
@@ -1,137 +1,142 @@
1
1
  import { jsx as R } from "react/jsx-runtime";
2
- import { createContext as b, useContext as Z, useRef as I, useState as N, useEffect as B, useCallback as w, useMemo as T } from "react";
3
- const x = 20, _ = 200, $ = b(null);
4
- function L() {
2
+ import { createContext as b, useContext as Z, useRef as h, useState as L, useEffect as E, useCallback as w, useMemo as N } from "react";
3
+ const x = 20, T = 200, $ = b(null);
4
+ function _() {
5
5
  return Z($);
6
6
  }
7
- function M({ mode: s, children: o, className: y, ...h }) {
8
- const e = L(), S = I(null), k = I(null), [r, n] = N(!1), t = I({
7
+ function F({ mode: u, children: f, className: g, ...k }) {
8
+ const e = _(), S = h(null), I = h(null), [r, m] = L(!1), t = h({
9
9
  fixedOffsetTop: (e == null ? void 0 : e.fixedOffsetTop) || 0,
10
10
  isSticky: r,
11
11
  baseZIndex: (e == null ? void 0 : e.baseZIndex) || x
12
12
  });
13
- return t.current.baseZIndex = (e == null ? void 0 : e.baseZIndex) || x, t.current.fixedOffsetTop = (e == null ? void 0 : e.fixedOffsetTop) || 0, t.current.isSticky = r, B(() => {
14
- const u = k.current, f = S.current, d = s || (e == null ? void 0 : e.mode);
15
- if (!e || !f || !u || d === "none" || !d || !e.register) {
16
- n(!1);
13
+ return t.current.baseZIndex = (e == null ? void 0 : e.baseZIndex) || x, t.current.fixedOffsetTop = (e == null ? void 0 : e.fixedOffsetTop) || 0, t.current.isSticky = r, E(() => {
14
+ const n = I.current, a = S.current, l = u || (e == null ? void 0 : e.mode);
15
+ if (!e || !a || !n || l === "none" || !l || !e.register) {
16
+ m(!1);
17
17
  return;
18
18
  }
19
- const a = {
20
- el: f,
21
- update: (l, i, p, C, H) => {
22
- if (!l)
23
- return t.current.isSticky && n(!1), 0;
24
- if (i <= p) {
25
- const g = u.getBoundingClientRect().height;
26
- let v = p;
27
- if (d === "replace" && typeof C < "u") {
28
- const m = C - (p + g);
29
- if (m < 0 && (v = p + m, m + g < 0))
30
- return t.current.isSticky && n(!1), 0;
31
- }
32
- return t.current.isSticky || n(!0), u.style.top = `${v}px`, u.style.width = `${f.offsetWidth}px`, u.style.zIndex = `${t.current.baseZIndex + (d === "replace" ? -H : H)}`, f.style.height = `${g}px`, d === "replace" ? 0 : g;
33
- } else
34
- n(!1);
35
- return 0;
19
+ const s = {
20
+ el: a,
21
+ update: (d, c, i, o, C) => {
22
+ if (!d || c > i)
23
+ return t.current.isSticky && m(!1), 0;
24
+ const p = n.getBoundingClientRect().height;
25
+ let H = i;
26
+ if (l === "replace" && typeof o < "u") {
27
+ const y = o - (i + p);
28
+ if (y < 0 && (H = i + y, y + p < 0))
29
+ return t.current.isSticky && m(!1), 0;
30
+ }
31
+ return a.style.height = `${p}px`, t.current.isSticky || m(!0), n.style.top = `${H}px`, n.style.width = `${a.offsetWidth}px`, n.style.zIndex = `${t.current.baseZIndex + (l === "replace" ? -C : C)}`, l === "replace" ? 0 : p;
36
32
  }
37
33
  };
38
- return e.register(a);
39
- }, [e == null ? void 0 : e.mode, e == null ? void 0 : e.register, s]), B(() => {
40
- const u = S.current, f = k.current, d = e == null ? void 0 : e.updateStickyItemsHeight;
41
- if (!r || !u || !f || !d)
34
+ return e.register(s);
35
+ }, [e == null ? void 0 : e.mode, e == null ? void 0 : e.register, u]), E(() => {
36
+ const n = S.current, a = I.current, l = e == null ? void 0 : e.updateStickyItemsHeight;
37
+ if (!r || !n || !a || !l)
42
38
  return;
43
- const c = d(f.getBoundingClientRect().height);
39
+ const v = l(a.getBoundingClientRect().height);
44
40
  return () => {
45
- O(u, ["height"]), O(f, ["top", "z-index", "width"]), c();
41
+ O(n, ["height"]), O(a, ["top", "z-index", "width"]), v();
46
42
  };
47
- }, [r, e == null ? void 0 : e.updateStickyItemsHeight]), /* @__PURE__ */ R("div", { className: "oe-sticky-item " + (y || ""), ...h, ref: S, children: /* @__PURE__ */ R("div", { className: "oe-sticky-content " + (r ? "is-sticky" : ""), ref: k, children: o }) });
43
+ }, [r, e == null ? void 0 : e.updateStickyItemsHeight]), /* @__PURE__ */ R("div", { className: "oe-sticky-item " + (g || ""), ...k, ref: S, children: /* @__PURE__ */ R("div", { className: "oe-sticky-content " + (r ? "is-sticky" : ""), ref: I, children: f }) });
48
44
  }
49
- function O(s, o) {
50
- o.forEach((y) => {
51
- s.style.removeProperty(y);
45
+ function O(u, f) {
46
+ f.forEach((g) => {
47
+ u.style.removeProperty(g);
52
48
  });
53
49
  }
54
- function P({
55
- children: s,
56
- offsetTop: o = 0,
57
- baseZIndex: y,
58
- onStickyItemsHeightChange: h,
50
+ function M({
51
+ children: u,
52
+ offsetTop: f = 0,
53
+ baseZIndex: g,
54
+ onStickyItemsHeightChange: k,
59
55
  defaultMode: e = "replace",
60
56
  ...S
61
57
  }) {
62
- const k = I(null), r = I([]), n = I({
63
- fixedOffsetTop: o,
58
+ const I = h(null), r = h([]), m = h(null), t = h({
59
+ fixedOffsetTop: f,
64
60
  defaultMode: e,
65
61
  stickyItemsHeight: 0,
66
- onStickyItemsHeightChange: h
62
+ onStickyItemsHeightChange: k,
63
+ lastCanSticky: !1
67
64
  });
68
- n.current.onStickyItemsHeightChange = h;
69
- const t = w(() => {
70
- const c = k.current;
71
- if (!c)
72
- return;
73
- const a = c.getBoundingClientRect(), l = n.current, i = l.fixedOffsetTop, p = l.stickyItemsHeight, C = !(a.top > i || a.bottom < i);
74
- let H = i, g = a.bottom - (i + p);
75
- g > 0 && (g = 0);
76
- const v = r.current.map((m) => m.el.getBoundingClientRect().top);
77
- r.current.forEach((m, E) => {
78
- H += m.update(C, v[E], H + g, v[E + 1], E);
79
- });
80
- }, []), u = w((c) => {
81
- const a = n.current.stickyItemsHeight + c;
82
- return n.current.stickyItemsHeight = a, setTimeout(() => {
83
- var l, i;
84
- (i = (l = n.current).onStickyItemsHeightChange) == null || i.call(l, a);
65
+ t.current.onStickyItemsHeightChange = k;
66
+ const n = w(() => {
67
+ const s = I.current;
68
+ !s || m.current || (m.current = requestAnimationFrame(() => {
69
+ m.current = null;
70
+ const d = s.getBoundingClientRect(), { fixedOffsetTop: c, stickyItemsHeight: i } = t.current, o = !(d.top > c || d.bottom < c);
71
+ if (!o) {
72
+ t.current.lastCanSticky !== o && (s.classList.toggle("can-sticky", !1), r.current.forEach((y) => y.update(!1, 0, 0, 0, 0)), t.current.lastCanSticky = o);
73
+ return;
74
+ }
75
+ t.current.lastCanSticky = o, i > 0 && s.classList.toggle("can-sticky", !0);
76
+ let C = c, p = d.bottom - (c + i);
77
+ p > 0 && (p = 0);
78
+ const H = r.current.map((y) => y.el.getBoundingClientRect().top);
79
+ r.current.forEach((y, B) => {
80
+ C += y.update(o, H[B], C + p, H[B + 1], B);
81
+ });
82
+ }));
83
+ }, []), a = w((s) => {
84
+ const d = t.current.stickyItemsHeight + s;
85
+ return t.current.stickyItemsHeight = d, setTimeout(() => {
86
+ var c, i;
87
+ (i = (c = t.current).onStickyItemsHeightChange) == null || i.call(c, d);
85
88
  }, 0), () => {
86
- const l = n.current.stickyItemsHeight - c;
87
- n.current.stickyItemsHeight = l, setTimeout(() => {
88
- var i, p;
89
- (p = (i = n.current).onStickyItemsHeightChange) == null || p.call(i, l);
89
+ const c = t.current.stickyItemsHeight - s;
90
+ t.current.stickyItemsHeight = c, setTimeout(() => {
91
+ var i, o;
92
+ (o = (i = t.current).onStickyItemsHeightChange) == null || o.call(i, c);
90
93
  }, 0);
91
94
  };
92
95
  }, []);
93
- B(() => {
94
- n.current.fixedOffsetTop = o, n.current.defaultMode = e, t();
95
- }, [o, e, t]);
96
- const f = w((c) => (r.current.push(c), r.current = A(r.current), t(), () => {
97
- r.current = r.current.filter((a) => a !== c), t();
98
- }), [t]);
99
- B(() => (window.addEventListener("scroll", t, { passive: !0 }), window.addEventListener("resize", t, { passive: !0 }), () => {
100
- window.removeEventListener("scroll", t), window.removeEventListener("resize", t);
101
- }), [t]);
102
- const d = T(() => z(y), [y]);
96
+ E(() => {
97
+ t.current.fixedOffsetTop = f, t.current.defaultMode = e, setTimeout(() => {
98
+ n();
99
+ }, 10);
100
+ }, [f, e, n]);
101
+ const l = w((s) => (r.current.push(s), r.current = z(r.current), n(), () => {
102
+ r.current = r.current.filter((d) => d !== s), n();
103
+ }), [n]);
104
+ E(() => (window.addEventListener("scroll", n, { passive: !0 }), window.addEventListener("resize", n, { passive: !0 }), () => {
105
+ window.removeEventListener("scroll", n), window.removeEventListener("resize", n);
106
+ }), [n]);
107
+ const v = N(() => A(g), [g]);
103
108
  return /* @__PURE__ */ R(
104
109
  $.Provider,
105
110
  {
106
111
  value: {
107
- register: f,
108
- baseZIndex: d,
109
- updateStickyItemsHeight: u,
110
- fixedOffsetTop: o,
112
+ register: l,
113
+ baseZIndex: v,
114
+ updateStickyItemsHeight: a,
115
+ fixedOffsetTop: f,
111
116
  mode: e
112
117
  },
113
118
  children: /* @__PURE__ */ R(
114
119
  "div",
115
120
  {
116
121
  ...S,
117
- ref: k,
122
+ ref: I,
118
123
  className: "oe-sticky-container",
119
- children: s
124
+ children: u
120
125
  }
121
126
  )
122
127
  }
123
128
  );
124
129
  }
125
- function z(s) {
126
- return typeof s > "u" ? _ : Math.max(Number(s) || 0, x);
130
+ function A(u) {
131
+ return typeof u > "u" ? T : Math.max(Number(u) || 0, x);
127
132
  }
128
- function A(s) {
129
- return s.sort((o, y) => {
130
- const h = o.el.getBoundingClientRect(), e = y.el.getBoundingClientRect();
131
- return h.top - e.top;
133
+ function z(u) {
134
+ return u.sort((f, g) => {
135
+ const k = f.el.getBoundingClientRect(), e = g.el.getBoundingClientRect();
136
+ return k.top - e.top;
132
137
  });
133
138
  }
134
139
  export {
135
- P as StickyContainer,
136
- M as StickyItem
140
+ M as StickyContainer,
141
+ F as StickyItem
137
142
  };
@@ -1 +1 @@
1
- (function(p,g){typeof exports=="object"&&typeof module<"u"?g(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],g):(p=typeof globalThis<"u"?globalThis:p||self,g(p.ReactSticky={},p.jsxRuntime,p.React))})(this,function(p,g,n){"use strict";const B=n.createContext(null);function N(){return n.useContext(B)}const $="";function O({mode:c,children:o,className:m,...S}){const e=N(),R=n.useRef(null),E=n.useRef(null),[s,i]=n.useState(!1),t=n.useRef({fixedOffsetTop:(e==null?void 0:e.fixedOffsetTop)||0,isSticky:s,baseZIndex:(e==null?void 0:e.baseZIndex)||20});return t.current.baseZIndex=(e==null?void 0:e.baseZIndex)||20,t.current.fixedOffsetTop=(e==null?void 0:e.fixedOffsetTop)||0,t.current.isSticky=s,n.useEffect(()=>{const f=E.current,d=R.current,l=c||(e==null?void 0:e.mode);if(!e||!d||!f||l==="none"||!l||!e.register){i(!1);return}const y={el:d,update:(a,r,h,C,H)=>{if(!a)return t.current.isSticky&&i(!1),0;if(r<=h){const I=f.getBoundingClientRect().height;let _=h;if(l==="replace"&&typeof C<"u"){const k=C-(h+I);if(k<0&&(_=h+k,k+I<0))return t.current.isSticky&&i(!1),0}return t.current.isSticky||i(!0),f.style.top=`${_}px`,f.style.width=`${d.offsetWidth}px`,f.style.zIndex=`${t.current.baseZIndex+(l==="replace"?-H:H)}`,d.style.height=`${I}px`,l==="replace"?0:I}else i(!1);return 0}};return e.register(y)},[e==null?void 0:e.mode,e==null?void 0:e.register,c]),n.useEffect(()=>{const f=R.current,d=E.current,l=e==null?void 0:e.updateStickyItemsHeight;if(!s||!f||!d||!l)return;const u=l(d.getBoundingClientRect().height);return()=>{x(f,["height"]),x(d,["top","z-index","width"]),u()}},[s,e==null?void 0:e.updateStickyItemsHeight]),g.jsx("div",{className:"oe-sticky-item "+(m||""),...S,ref:R,children:g.jsx("div",{className:"oe-sticky-content "+(s?"is-sticky":""),ref:E,children:o})})}function x(c,o){o.forEach(m=>{c.style.removeProperty(m)})}function Z({children:c,offsetTop:o=0,baseZIndex:m,onStickyItemsHeightChange:S,defaultMode:e="replace",...R}){const E=n.useRef(null),s=n.useRef([]),i=n.useRef({fixedOffsetTop:o,defaultMode:e,stickyItemsHeight:0,onStickyItemsHeightChange:S});i.current.onStickyItemsHeightChange=S;const t=n.useCallback(()=>{const u=E.current;if(!u)return;const y=u.getBoundingClientRect(),a=i.current,r=a.fixedOffsetTop,h=a.stickyItemsHeight,C=!(y.top>r||y.bottom<r);let H=r,I=y.bottom-(r+h);I>0&&(I=0);const _=s.current.map(k=>k.el.getBoundingClientRect().top);s.current.forEach((k,v)=>{H+=k.update(C,_[v],H+I,_[v+1],v)})},[]),f=n.useCallback(u=>{const y=i.current.stickyItemsHeight+u;return i.current.stickyItemsHeight=y,setTimeout(()=>{var a,r;(r=(a=i.current).onStickyItemsHeightChange)==null||r.call(a,y)},0),()=>{const a=i.current.stickyItemsHeight-u;i.current.stickyItemsHeight=a,setTimeout(()=>{var r,h;(h=(r=i.current).onStickyItemsHeightChange)==null||h.call(r,a)},0)}},[]);n.useEffect(()=>{i.current.fixedOffsetTop=o,i.current.defaultMode=e,t()},[o,e,t]);const d=n.useCallback(u=>(s.current.push(u),s.current=w(s.current),t(),()=>{s.current=s.current.filter(y=>y!==u),t()}),[t]);n.useEffect(()=>(window.addEventListener("scroll",t,{passive:!0}),window.addEventListener("resize",t,{passive:!0}),()=>{window.removeEventListener("scroll",t),window.removeEventListener("resize",t)}),[t]);const l=n.useMemo(()=>b(m),[m]);return g.jsx(B.Provider,{value:{register:d,baseZIndex:l,updateStickyItemsHeight:f,fixedOffsetTop:o,mode:e},children:g.jsx("div",{...R,ref:E,className:"oe-sticky-container",children:c})})}function b(c){return typeof c>"u"?200:Math.max(Number(c)||0,20)}function w(c){return c.sort((o,m)=>{const S=o.el.getBoundingClientRect(),e=m.el.getBoundingClientRect();return S.top-e.top})}p.StickyContainer=Z,p.StickyItem=O,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"})});
1
+ (function(p,m){typeof exports=="object"&&typeof module<"u"?m(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],m):(p=typeof globalThis<"u"?globalThis:p||self,m(p.ReactSticky={},p.jsxRuntime,p.React))})(this,function(p,m,r){"use strict";const B=r.createContext(null);function x(){return r.useContext(B)}const $="";function Z({mode:o,children:l,className:h,...S}){const e=x(),C=r.useRef(null),E=r.useRef(null),[s,k]=r.useState(!1),t=r.useRef({fixedOffsetTop:(e==null?void 0:e.fixedOffsetTop)||0,isSticky:s,baseZIndex:(e==null?void 0:e.baseZIndex)||20});return t.current.baseZIndex=(e==null?void 0:e.baseZIndex)||20,t.current.fixedOffsetTop=(e==null?void 0:e.fixedOffsetTop)||0,t.current.isSticky=s,r.useEffect(()=>{const n=E.current,d=C.current,a=o||(e==null?void 0:e.mode);if(!e||!d||!n||a==="none"||!a||!e.register){k(!1);return}const c={el:d,update:(y,u,i,f,R)=>{if(!y||u>i)return t.current.isSticky&&k(!1),0;const I=n.getBoundingClientRect().height;let _=i;if(a==="replace"&&typeof f<"u"){const g=f-(i+I);if(g<0&&(_=i+g,g+I<0))return t.current.isSticky&&k(!1),0}return d.style.height=`${I}px`,t.current.isSticky||k(!0),n.style.top=`${_}px`,n.style.width=`${d.offsetWidth}px`,n.style.zIndex=`${t.current.baseZIndex+(a==="replace"?-R:R)}`,a==="replace"?0:I}};return e.register(c)},[e==null?void 0:e.mode,e==null?void 0:e.register,o]),r.useEffect(()=>{const n=C.current,d=E.current,a=e==null?void 0:e.updateStickyItemsHeight;if(!s||!n||!d||!a)return;const H=a(d.getBoundingClientRect().height);return()=>{N(n,["height"]),N(d,["top","z-index","width"]),H()}},[s,e==null?void 0:e.updateStickyItemsHeight]),m.jsx("div",{className:"oe-sticky-item "+(h||""),...S,ref:C,children:m.jsx("div",{className:"oe-sticky-content "+(s?"is-sticky":""),ref:E,children:l})})}function N(o,l){l.forEach(h=>{o.style.removeProperty(h)})}function b({children:o,offsetTop:l=0,baseZIndex:h,onStickyItemsHeightChange:S,defaultMode:e="replace",...C}){const E=r.useRef(null),s=r.useRef([]),k=r.useRef(null),t=r.useRef({fixedOffsetTop:l,defaultMode:e,stickyItemsHeight:0,onStickyItemsHeightChange:S,lastCanSticky:!1});t.current.onStickyItemsHeightChange=S;const n=r.useCallback(()=>{const c=E.current;!c||k.current||(k.current=requestAnimationFrame(()=>{k.current=null;const y=c.getBoundingClientRect(),{fixedOffsetTop:u,stickyItemsHeight:i}=t.current,f=!(y.top>u||y.bottom<u);if(!f){t.current.lastCanSticky!==f&&(c.classList.toggle("can-sticky",!1),s.current.forEach(g=>g.update(!1,0,0,0,0)),t.current.lastCanSticky=f);return}t.current.lastCanSticky=f,i>0&&c.classList.toggle("can-sticky",!0);let R=u,I=y.bottom-(u+i);I>0&&(I=0);const _=s.current.map(g=>g.el.getBoundingClientRect().top);s.current.forEach((g,v)=>{R+=g.update(f,_[v],R+I,_[v+1],v)})}))},[]),d=r.useCallback(c=>{const y=t.current.stickyItemsHeight+c;return t.current.stickyItemsHeight=y,setTimeout(()=>{var u,i;(i=(u=t.current).onStickyItemsHeightChange)==null||i.call(u,y)},0),()=>{const u=t.current.stickyItemsHeight-c;t.current.stickyItemsHeight=u,setTimeout(()=>{var i,f;(f=(i=t.current).onStickyItemsHeightChange)==null||f.call(i,u)},0)}},[]);r.useEffect(()=>{t.current.fixedOffsetTop=l,t.current.defaultMode=e,setTimeout(()=>{n()},10)},[l,e,n]);const a=r.useCallback(c=>(s.current.push(c),s.current=O(s.current),n(),()=>{s.current=s.current.filter(y=>y!==c),n()}),[n]);r.useEffect(()=>(window.addEventListener("scroll",n,{passive:!0}),window.addEventListener("resize",n,{passive:!0}),()=>{window.removeEventListener("scroll",n),window.removeEventListener("resize",n)}),[n]);const H=r.useMemo(()=>w(h),[h]);return m.jsx(B.Provider,{value:{register:a,baseZIndex:H,updateStickyItemsHeight:d,fixedOffsetTop:l,mode:e},children:m.jsx("div",{...C,ref:E,className:"oe-sticky-container",children:o})})}function w(o){return typeof o>"u"?200:Math.max(Number(o)||0,20)}function O(o){return o.sort((l,h)=>{const S=l.el.getBoundingClientRect(),e=h.el.getBoundingClientRect();return S.top-e.top})}p.StickyContainer=b,p.StickyItem=Z,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"})});
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- .oe-sticky-container,.oe-sticky-item{position:relative}.oe-sticky-content{will-change:top,position}.oe-sticky-content.is-sticky{position:fixed}
1
+ .oe-sticky-container{position:relative}.oe-sticky-container.can-sticky .oe-sticky-content.is-sticky{position:fixed}.oe-sticky-item{position:relative}.oe-sticky-content{will-change:top,position}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-sticky-kit",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "A lightweight, flexible React sticky container and item component library supporting multiple sticky modes, advanced layouts, and edge cases.",
6
6
  "keywords": [
@@ -26,6 +26,9 @@
26
26
  "preview": "vite preview",
27
27
  "type-check": "tsc --noEmit",
28
28
  "lint": "eslint src demo --ext .ts,.tsx,.js,.jsx",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "coverage": "vitest run --coverage",
29
32
  "pub": "tsx ./scripts/publish.ts"
30
33
  },
31
34
  "files": [
@@ -36,9 +39,9 @@
36
39
  "types": "dist/index.d.ts",
37
40
  "exports": {
38
41
  ".": {
42
+ "types": "./dist/index.d.ts",
39
43
  "import": "./dist/react-sticky.es.js",
40
- "require": "./dist/react-sticky.umd.js",
41
- "types": "./dist/index.d.ts"
44
+ "require": "./dist/react-sticky.umd.js"
42
45
  },
43
46
  "./style": "./dist/style.css",
44
47
  "./dist/style.css": "./dist/style.css"
@@ -49,25 +52,33 @@
49
52
  },
50
53
  "devDependencies": {
51
54
  "@eslint/js": "^9.25.0",
55
+ "@testing-library/jest-dom": "^6.1.6",
56
+ "@testing-library/react": "^14.1.2",
57
+ "@testing-library/user-event": "^14.4.3",
52
58
  "@types/node": "^22.15.18",
53
59
  "@types/react": "^19.1.2",
54
60
  "@types/react-dom": "^19.1.2",
55
61
  "@typescript-eslint/eslint-plugin": "^8.32.1",
56
62
  "@typescript-eslint/parser": "^8.32.1",
57
63
  "@vitejs/plugin-react": "^4.0.0",
64
+ "@vitest/coverage-v8": "^3.1.3",
58
65
  "eslint": "^9.26.0",
59
66
  "eslint-config-prettier": "^10.1.5",
60
67
  "eslint-plugin-jsx-a11y": "^6.8.0",
61
68
  "eslint-plugin-react": "^7.33.2",
62
69
  "eslint-plugin-react-hooks": "^5.2.0",
63
70
  "globals": "^16.0.0",
71
+ "identity-obj-proxy": "^3.0.0",
72
+ "jsdom": "^26.1.0",
64
73
  "react": "^19.1.0",
65
74
  "react-dom": "^19.1.0",
66
75
  "sass": "^1.88.0",
76
+ "ts-node": "^10.9.2",
67
77
  "tsx": "^4.19.4",
68
78
  "typescript": "^5.0.0",
69
79
  "typescript-eslint": "^8.30.1",
70
80
  "vite": "^4.0.0",
71
- "vite-plugin-dts": "^4.5.4"
81
+ "vite-plugin-dts": "^4.5.4",
82
+ "vitest": "^3.1.3"
72
83
  }
73
84
  }