react-sticky-kit 0.1.5 → 0.2.0

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,8 @@ 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
+ - 🖥️ Flexible reference containers (window, DOM elements, refs)
35
+ - 🔄 Supports SSR/SSG (Next.js, Gatsby, Astro, etc.)
25
36
  - 🧪 Handles edge cases: empty sections, dynamic heights, zero-height headers, long headers, etc.
26
37
  - ⚡️ Written in TypeScript, fully typed
27
38
  - 🧪 Includes demo pages for real-world scenarios
@@ -36,6 +47,9 @@ yarn add react-sticky-kit
36
47
  pnpm add react-sticky-kit
37
48
  ```
38
49
 
50
+ ## Demo
51
+ - [Apple iOS Contact App](https://codesandbox.io/p/sandbox/dreamy-hofstadter-v9dzfz)
52
+
39
53
  ## Usage
40
54
 
41
55
  ```tsx
@@ -63,13 +77,14 @@ export default function Example() {
63
77
  ## Props
64
78
 
65
79
  ### `<StickyContainer />`
66
- | Prop | Type | Default | Description |
67
- |-----------------------------|--------------------------------------|-------------|---------------------------------------------------------------------------------------------|
68
- | `offsetTop` | `number` | `0` | Offset from the top of the viewport |
69
- | `defaultMode` | `'replace' \| 'stack' \| 'none'` | `'replace'`| Default sticky mode for all items |
70
- | `baseZIndex` | `number` | `200` | Base z-index for sticky items. Should be greater than the number of sticky items. |
71
- | | | | In `replace` mode, z-index = baseZIndex - index; in `stack` mode, z-index = baseZIndex + index. |
72
- | `onStickyItemsHeightChange` | `(height: number) => void` | | Callback when total sticky height changes |
80
+ | Prop | Type | Default | Description |
81
+ |-----------------------------|------------------------------------------------------|-------------|---------------------------------------------------------------------------------------------|
82
+ | `offsetTop` | `number` | `0` | Offset from the top of the viewport |
83
+ | `defaultMode` | `'replace' \| 'stack' \| 'none'` | `'replace'` | Default sticky mode for all items |
84
+ | `baseZIndex` | `number` | `200` | Base z-index for sticky items. Should be greater than the number of sticky items. |
85
+ | | | | In `replace` mode, z-index = baseZIndex - index; in `stack` mode, z-index = baseZIndex + index. |
86
+ | `onStickyItemsHeightChange` | `(height: number) => void` | | Callback when total sticky height changes |
87
+ | `constraint` | `'none'` | | Define the constraint for sticky behavior. 'none' = no constraints (CSS-like behavior) |
73
88
 
74
89
  ### `<StickyItem />`
75
90
  | Prop | Type | Default | Description |
@@ -81,6 +96,44 @@ export default function Example() {
81
96
  - **stack**: Sticky items stack on top of each other.
82
97
  - **none**: Sticky is disabled for this item.
83
98
 
99
+ ## Constraint Behavior
100
+
101
+ The `constraint` prop allows you to control when sticky items should stop being sticky:
102
+
103
+ ```tsx
104
+ import React from 'react';
105
+ import { StickyContainer, StickyItem } from 'react-sticky-kit';
106
+
107
+ function Example() {
108
+ return (
109
+ <div>
110
+ {/* 1. Default: Container-based constraint */}
111
+ <StickyContainer>
112
+ <StickyItem><div>Sticky stops when container leaves viewport</div></StickyItem>
113
+ <div>Content...</div>
114
+ </StickyContainer>
115
+
116
+ {/* 2. No constraints (like CSS position:sticky) */}
117
+ <StickyContainer constraint="none">
118
+ <StickyItem><div>Always sticky like CSS position:sticky</div></StickyItem>
119
+ <div>Content...</div>
120
+ </StickyContainer>
121
+ </div>
122
+ );
123
+ }
124
+ <div>Content...</div>
125
+ </StickyContainer>
126
+ </div>
127
+ );
128
+ }
129
+ ```
130
+
131
+ ### Special behavior of `constraint="none"`
132
+
133
+ When you set `constraint="none"`, the sticky items will always stick when they reach the top of the viewport, regardless of their parent container's visibility. This exactly matches the behavior of native CSS `position: sticky`.
134
+
135
+ In contrast, the default behavior (without specifying a constraint) only makes items sticky when their parent container is visible in the viewport.
136
+
84
137
  ## Demo
85
138
 
86
139
  Run the demo locally:
@@ -93,8 +146,55 @@ pnpm dev
93
146
  Open [http://localhost:5173](http://localhost:5173) and switch between demo pages to explore all features and edge cases:
94
147
  * [iOS Contact](http://localhost:5173/#ios-contact) a dead simple iOS contact clone
95
148
  * [Mixed mode](http://localhost:5173/#mixed-mode) mix replace/stack/none mode
96
- * [Nested](http://localhost:5173/#dynamic-height) nest sticky containers
97
- * [Dynamic sticky items](http://localhost:5173/#nested) dynamic sticky items(add/remove)
149
+ * [Nested](http://localhost:5173/#nested) nest sticky containers
150
+ * [Dynamic sticky items](http://localhost:5173/#dynamic) dynamic sticky items(add/remove)
151
+ * [Dynamic offsetTop](http://localhost:5173/#dynamic-offset) dynamic offsetTop that adapts to header height changes
152
+ * [Constraint Demo](http://localhost:5173/#constraint) demo showing different constraint options
153
+
154
+ ## SSR/SSG Support
155
+
156
+ React Sticky Kit supports Server-Side Rendering (SSR) and Static Site Generation (SSG), working seamlessly with frameworks like Next.js, Gatsby, Astro, and more.
157
+
158
+ ### Next.js Example
159
+
160
+ ```tsx
161
+ // pages/index.tsx
162
+ import { StickyContainer, StickyItem } from 'react-sticky-kit'
163
+ import 'react-sticky-kit/dist/style.css'
164
+
165
+ export default function Home() {
166
+ return (
167
+ <StickyContainer>
168
+ <StickyItem>
169
+ <header>Sticky Header</header>
170
+ </StickyItem>
171
+ <main>Content...</main>
172
+ </StickyContainer>
173
+ )
174
+ }
175
+ ```
176
+
177
+ ### Astro Example
178
+
179
+ ```astro
180
+ ---
181
+ // src/pages/index.astro
182
+ import { StickyContainer, StickyItem } from 'react-sticky-kit'
183
+ import 'react-sticky-kit/dist/style.css'
184
+ ---
185
+
186
+ <html>
187
+ <head>...</head>
188
+ <body>
189
+ <StickyContainer client:load>
190
+ <StickyItem>
191
+ <header>Sticky Header</header>
192
+ </StickyItem>
193
+ <div>Content...</div>
194
+ </StickyContainer>
195
+ </body>
196
+ </html>
197
+ ```
98
198
 
99
199
  ## License
100
200
 
package/dist/index.d.ts CHANGED
@@ -25,6 +25,12 @@ export declare interface IStickyContainerProps extends default_2.HTMLAttributes<
25
25
  * @param height The total height of all sticky items inside the container.
26
26
  */
27
27
  onStickyItemsHeightChange?: (height: number) => void;
28
+ /**
29
+ * Define the constraint for sticky behavior
30
+ * - undefined (default): Sticky items will stop being sticky when StickyContainer leaves viewport
31
+ * - 'none': No constraints, similar to CSS position:sticky behavior
32
+ */
33
+ constraint?: 'none';
28
34
  }
29
35
 
30
36
  export declare interface IStickyItemProps extends default_2.HTMLAttributes<HTMLDivElement> {
@@ -43,7 +49,7 @@ export declare interface IStickyItemProps extends default_2.HTMLAttributes<HTMLD
43
49
  */
44
50
  export declare type IStickyMode = 'replace' | 'stack' | 'none';
45
51
 
46
- export declare function StickyContainer({ children, offsetTop, baseZIndex, onStickyItemsHeightChange, defaultMode, ...rest }: IStickyContainerProps): JSX.Element;
52
+ export declare function StickyContainer({ children, offsetTop, baseZIndex, onStickyItemsHeightChange, defaultMode, constraint, ...rest }: IStickyContainerProps): JSX.Element;
47
53
 
48
54
  export declare function StickyItem({ mode, children, className, ...rest }: IStickyItemProps): JSX.Element;
49
55
 
@@ -1,142 +1,154 @@
1
- import { jsx as R } from "react/jsx-runtime";
2
- import { createContext as b, useContext as T, useRef as S, useState as Z, useEffect as E, useCallback as w, useMemo as L } from "react";
3
- const x = 20, N = 200, $ = b(null);
4
- function _() {
5
- return T($);
1
+ import { jsx as w } from "react/jsx-runtime";
2
+ import { createContext as Z, useContext as L, useRef as k, useState as T, useEffect as E, useCallback as v, useMemo as _ } from "react";
3
+ const O = 20, A = 200, $ = Z(null);
4
+ function z() {
5
+ return L($);
6
6
  }
7
- function M({ mode: c, children: o, className: p, ...k }) {
8
- const e = _(), C = S(null), I = S(null), [r, t] = Z(!1), n = S({
7
+ function M({ mode: f, children: y, className: h, ...I }) {
8
+ const e = z(), g = k(null), S = k(null), [m, o] = T(!1), l = k({
9
9
  fixedOffsetTop: (e == null ? void 0 : e.fixedOffsetTop) || 0,
10
- isSticky: r,
11
- baseZIndex: (e == null ? void 0 : e.baseZIndex) || x
10
+ isSticky: m,
11
+ baseZIndex: (e == null ? void 0 : e.baseZIndex) || O
12
12
  });
13
- return n.current.baseZIndex = (e == null ? void 0 : e.baseZIndex) || x, n.current.fixedOffsetTop = (e == null ? void 0 : e.fixedOffsetTop) || 0, n.current.isSticky = r, E(() => {
14
- const u = I.current, f = C.current, a = c || (e == null ? void 0 : e.mode);
15
- if (!e || !f || !u || a === "none" || !a || !e.register) {
16
- t(!1);
13
+ 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(() => {
14
+ const t = S.current, a = g.current, n = f || (e == null ? void 0 : e.mode);
15
+ if (!e || !a || !t || n === "none" || !n || !e.register) {
16
+ o(!1);
17
17
  return;
18
18
  }
19
- const l = {
20
- el: f,
21
- update: (d, i, y, m, H) => {
22
- if (!d)
23
- return n.current.isSticky && t(!1), 0;
24
- if (i <= y) {
25
- const h = u.getBoundingClientRect().height;
26
- let v = y;
27
- if (a === "replace" && typeof m < "u") {
28
- const g = m - (y + h);
29
- if (g < 0 && (v = y + g, g + h < 0))
30
- return n.current.isSticky && t(!1), 0;
31
- }
32
- return f.style.height = `${h}px`, n.current.isSticky || t(!0), u.style.top = `${v}px`, u.style.width = `${f.offsetWidth}px`, u.style.zIndex = `${n.current.baseZIndex + (a === "replace" ? -H : H)}`, a === "replace" ? 0 : h;
33
- } else
34
- t(!1);
35
- return 0;
19
+ const x = {
20
+ el: a,
21
+ update: (B, c, s, r, u) => {
22
+ if (!B || c > s)
23
+ return l.current.isSticky && o(!1), 0;
24
+ const i = t.getBoundingClientRect().height;
25
+ let p = s;
26
+ if (n === "replace" && typeof r < "u") {
27
+ const d = r - (s + i);
28
+ if (d < 0 && (p = s + d, d + i < 0))
29
+ return l.current.isSticky && o(!1), 0;
30
+ }
31
+ 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;
36
32
  }
37
33
  };
38
- return e.register(l);
39
- }, [e == null ? void 0 : e.mode, e == null ? void 0 : e.register, c]), E(() => {
40
- const u = C.current, f = I.current, a = e == null ? void 0 : e.updateStickyItemsHeight;
41
- if (!r || !u || !f || !a)
34
+ return e.register(x);
35
+ }, [e == null ? void 0 : e.mode, e == null ? void 0 : e.register, f]), E(() => {
36
+ const t = g.current, a = S.current, n = e == null ? void 0 : e.updateStickyItemsHeight;
37
+ if (!m || !t || !a || !n)
42
38
  return;
43
- const s = a(f.getBoundingClientRect().height);
39
+ const H = n(a.getBoundingClientRect().height);
44
40
  return () => {
45
- O(u, ["height"]), O(f, ["top", "z-index", "width"]), s();
41
+ N(t, ["height"]), N(a, ["top", "z-index", "width"]), H();
46
42
  };
47
- }, [r, e == null ? void 0 : e.updateStickyItemsHeight]), /* @__PURE__ */ R("div", { className: "oe-sticky-item " + (p || ""), ...k, ref: C, children: /* @__PURE__ */ R("div", { className: "oe-sticky-content " + (r ? "is-sticky" : ""), ref: I, children: o }) });
43
+ }, [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 }) });
48
44
  }
49
- function O(c, o) {
50
- o.forEach((p) => {
51
- c.style.removeProperty(p);
45
+ function N(f, y) {
46
+ y.forEach((h) => {
47
+ f.style.removeProperty(h);
52
48
  });
53
49
  }
54
50
  function P({
55
- children: c,
56
- offsetTop: o = 0,
57
- baseZIndex: p,
58
- onStickyItemsHeightChange: k,
51
+ children: f,
52
+ offsetTop: y = 0,
53
+ baseZIndex: h,
54
+ onStickyItemsHeightChange: I,
59
55
  defaultMode: e = "replace",
60
- ...C
56
+ constraint: g,
57
+ ...S
61
58
  }) {
62
- const I = S(null), r = S([]), t = S({
63
- fixedOffsetTop: o,
59
+ const m = k(null), o = k([]), l = k(null), t = k({
60
+ fixedOffsetTop: y,
64
61
  defaultMode: e,
65
62
  stickyItemsHeight: 0,
66
- onStickyItemsHeightChange: k,
63
+ onStickyItemsHeightChange: I,
67
64
  lastCanSticky: !1
68
65
  });
69
- t.current.onStickyItemsHeightChange = k;
70
- const n = w(() => {
71
- const s = I.current;
72
- if (!s)
73
- return;
74
- const l = s.getBoundingClientRect(), d = t.current, i = d.fixedOffsetTop, y = d.stickyItemsHeight, m = !(l.top > i || l.bottom < i);
75
- if (!m) {
76
- t.current.lastCanSticky !== m && (s.classList.toggle("can-sticky", !1), r.current.forEach((g) => g.update(!1, 0, 0, 0, 0)), t.current.lastCanSticky = m);
77
- return;
78
- }
79
- t.current.lastCanSticky = m, y > 0 && s.classList.toggle("can-sticky", !0);
80
- let H = i, h = l.bottom - (i + y);
81
- h > 0 && (h = 0);
82
- const v = r.current.map((g) => g.el.getBoundingClientRect().top);
83
- r.current.forEach((g, B) => {
84
- H += g.update(m, v[B], H + h, v[B + 1], B);
85
- });
86
- }, []), u = w((s) => {
87
- const l = t.current.stickyItemsHeight + s;
88
- return t.current.stickyItemsHeight = l, setTimeout(() => {
89
- var d, i;
90
- (i = (d = t.current).onStickyItemsHeightChange) == null || i.call(d, l);
66
+ t.current.onStickyItemsHeightChange = I;
67
+ const a = v(() => {
68
+ var c;
69
+ return g === "none" ? {
70
+ top: -1 / 0,
71
+ bottom: 1 / 0,
72
+ left: -1 / 0,
73
+ right: 1 / 0,
74
+ width: 1 / 0,
75
+ height: 1 / 0,
76
+ x: 0,
77
+ y: 0,
78
+ toJSON: () => ({})
79
+ } : ((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: () => ({}) };
80
+ }, [g]), n = v(() => {
81
+ const c = m.current;
82
+ !c || l.current || (l.current = requestAnimationFrame(() => {
83
+ l.current = null;
84
+ const s = a(), { fixedOffsetTop: r, stickyItemsHeight: u } = t.current;
85
+ let i;
86
+ if (g === "none" ? i = !0 : i = !(s.top > r || s.bottom < r), !i) {
87
+ 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);
88
+ return;
89
+ }
90
+ t.current.lastCanSticky = i, u > 0 && c.classList.toggle("can-sticky", !0);
91
+ let p, d = 0;
92
+ p = r, d = s.bottom - (r + u), d > 0 && (d = 0);
93
+ const b = o.current.map((C) => C.el.getBoundingClientRect().top);
94
+ o.current.forEach((C, R) => {
95
+ p += C.update(i, b[R], p + d, b[R + 1], R);
96
+ });
97
+ }));
98
+ }, [a, g]), H = v((c) => {
99
+ const s = t.current.stickyItemsHeight + c;
100
+ return t.current.stickyItemsHeight = s, setTimeout(() => {
101
+ var r, u;
102
+ (u = (r = t.current).onStickyItemsHeightChange) == null || u.call(r, s);
91
103
  }, 0), () => {
92
- const d = t.current.stickyItemsHeight - s;
93
- t.current.stickyItemsHeight = d, setTimeout(() => {
94
- var i, y;
95
- (y = (i = t.current).onStickyItemsHeightChange) == null || y.call(i, d);
104
+ const r = t.current.stickyItemsHeight - c;
105
+ t.current.stickyItemsHeight = r, setTimeout(() => {
106
+ var u, i;
107
+ (i = (u = t.current).onStickyItemsHeightChange) == null || i.call(u, r);
96
108
  }, 0);
97
109
  };
98
110
  }, []);
99
111
  E(() => {
100
- t.current.fixedOffsetTop = o, t.current.defaultMode = e, setTimeout(() => {
112
+ t.current.fixedOffsetTop = y, t.current.defaultMode = e, setTimeout(() => {
101
113
  n();
102
114
  }, 10);
103
- }, [o, e, n]);
104
- const f = w((s) => (r.current.push(s), r.current = A(r.current), n(), () => {
105
- r.current = r.current.filter((l) => l !== s), n();
115
+ }, [y, e, n]);
116
+ const x = v((c) => (o.current.push(c), o.current = W(o.current), n(), () => {
117
+ o.current = o.current.filter((s) => s !== c), n();
106
118
  }), [n]);
107
119
  E(() => (window.addEventListener("scroll", n, { passive: !0 }), window.addEventListener("resize", n, { passive: !0 }), () => {
108
120
  window.removeEventListener("scroll", n), window.removeEventListener("resize", n);
109
121
  }), [n]);
110
- const a = L(() => z(p), [p]);
111
- return /* @__PURE__ */ R(
122
+ const B = _(() => D(h), [h]);
123
+ return /* @__PURE__ */ w(
112
124
  $.Provider,
113
125
  {
114
126
  value: {
115
- register: f,
116
- baseZIndex: a,
117
- updateStickyItemsHeight: u,
118
- fixedOffsetTop: o,
127
+ register: x,
128
+ baseZIndex: B,
129
+ updateStickyItemsHeight: H,
130
+ fixedOffsetTop: y,
119
131
  mode: e
120
132
  },
121
- children: /* @__PURE__ */ R(
133
+ children: /* @__PURE__ */ w(
122
134
  "div",
123
135
  {
124
- ...C,
125
- ref: I,
136
+ ...S,
137
+ ref: m,
126
138
  className: "oe-sticky-container",
127
- children: c
139
+ children: f
128
140
  }
129
141
  )
130
142
  }
131
143
  );
132
144
  }
133
- function z(c) {
134
- return typeof c > "u" ? N : Math.max(Number(c) || 0, x);
145
+ function D(f) {
146
+ return typeof f > "u" ? A : Math.max(Number(f) || 0, O);
135
147
  }
136
- function A(c) {
137
- return c.sort((o, p) => {
138
- const k = o.el.getBoundingClientRect(), e = p.el.getBoundingClientRect();
139
- return k.top - e.top;
148
+ function W(f) {
149
+ return f.sort((y, h) => {
150
+ const I = y.el.getBoundingClientRect(), e = h.el.getBoundingClientRect();
151
+ return I.top - e.top;
140
152
  });
141
153
  }
142
154
  export {
@@ -1 +1 @@
1
- (function(g,k){typeof exports=="object"&&typeof module<"u"?k(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],k):(g=typeof globalThis<"u"?globalThis:g||self,k(g.ReactSticky={},g.jsxRuntime,g.React))})(this,function(g,k,s){"use strict";const B=s.createContext(null);function N(){return s.useContext(B)}const $="";function O({mode:u,children:o,className:m,...E}){const e=N(),R=s.useRef(null),C=s.useRef(null),[i,t]=s.useState(!1),n=s.useRef({fixedOffsetTop:(e==null?void 0:e.fixedOffsetTop)||0,isSticky:i,baseZIndex:(e==null?void 0:e.baseZIndex)||20});return n.current.baseZIndex=(e==null?void 0:e.baseZIndex)||20,n.current.fixedOffsetTop=(e==null?void 0:e.fixedOffsetTop)||0,n.current.isSticky=i,s.useEffect(()=>{const f=C.current,l=R.current,d=u||(e==null?void 0:e.mode);if(!e||!l||!f||d==="none"||!d||!e.register){t(!1);return}const a={el:l,update:(y,c,p,I,H)=>{if(!y)return n.current.isSticky&&t(!1),0;if(c<=p){const S=f.getBoundingClientRect().height;let _=p;if(d==="replace"&&typeof I<"u"){const h=I-(p+S);if(h<0&&(_=p+h,h+S<0))return n.current.isSticky&&t(!1),0}return l.style.height=`${S}px`,n.current.isSticky||t(!0),f.style.top=`${_}px`,f.style.width=`${l.offsetWidth}px`,f.style.zIndex=`${n.current.baseZIndex+(d==="replace"?-H:H)}`,d==="replace"?0:S}else t(!1);return 0}};return e.register(a)},[e==null?void 0:e.mode,e==null?void 0:e.register,u]),s.useEffect(()=>{const f=R.current,l=C.current,d=e==null?void 0:e.updateStickyItemsHeight;if(!i||!f||!l||!d)return;const r=d(l.getBoundingClientRect().height);return()=>{x(f,["height"]),x(l,["top","z-index","width"]),r()}},[i,e==null?void 0:e.updateStickyItemsHeight]),k.jsx("div",{className:"oe-sticky-item "+(m||""),...E,ref:R,children:k.jsx("div",{className:"oe-sticky-content "+(i?"is-sticky":""),ref:C,children:o})})}function x(u,o){o.forEach(m=>{u.style.removeProperty(m)})}function T({children:u,offsetTop:o=0,baseZIndex:m,onStickyItemsHeightChange:E,defaultMode:e="replace",...R}){const C=s.useRef(null),i=s.useRef([]),t=s.useRef({fixedOffsetTop:o,defaultMode:e,stickyItemsHeight:0,onStickyItemsHeightChange:E,lastCanSticky:!1});t.current.onStickyItemsHeightChange=E;const n=s.useCallback(()=>{const r=C.current;if(!r)return;const a=r.getBoundingClientRect(),y=t.current,c=y.fixedOffsetTop,p=y.stickyItemsHeight,I=!(a.top>c||a.bottom<c);if(!I){t.current.lastCanSticky!==I&&(r.classList.toggle("can-sticky",!1),i.current.forEach(h=>h.update(!1,0,0,0,0)),t.current.lastCanSticky=I);return}t.current.lastCanSticky=I,p>0&&r.classList.toggle("can-sticky",!0);let H=c,S=a.bottom-(c+p);S>0&&(S=0);const _=i.current.map(h=>h.el.getBoundingClientRect().top);i.current.forEach((h,v)=>{H+=h.update(I,_[v],H+S,_[v+1],v)})},[]),f=s.useCallback(r=>{const a=t.current.stickyItemsHeight+r;return t.current.stickyItemsHeight=a,setTimeout(()=>{var y,c;(c=(y=t.current).onStickyItemsHeightChange)==null||c.call(y,a)},0),()=>{const y=t.current.stickyItemsHeight-r;t.current.stickyItemsHeight=y,setTimeout(()=>{var c,p;(p=(c=t.current).onStickyItemsHeightChange)==null||p.call(c,y)},0)}},[]);s.useEffect(()=>{t.current.fixedOffsetTop=o,t.current.defaultMode=e,setTimeout(()=>{n()},10)},[o,e,n]);const l=s.useCallback(r=>(i.current.push(r),i.current=b(i.current),n(),()=>{i.current=i.current.filter(a=>a!==r),n()}),[n]);s.useEffect(()=>(window.addEventListener("scroll",n,{passive:!0}),window.addEventListener("resize",n,{passive:!0}),()=>{window.removeEventListener("scroll",n),window.removeEventListener("resize",n)}),[n]);const d=s.useMemo(()=>Z(m),[m]);return k.jsx(B.Provider,{value:{register:l,baseZIndex:d,updateStickyItemsHeight:f,fixedOffsetTop:o,mode:e},children:k.jsx("div",{...R,ref:C,className:"oe-sticky-container",children:u})})}function Z(u){return typeof u>"u"?200:Math.max(Number(u)||0,20)}function b(u){return u.sort((o,m)=>{const E=o.el.getBoundingClientRect(),e=m.el.getBoundingClientRect();return E.top-e.top})}g.StickyContainer=T,g.StickyItem=O,Object.defineProperty(g,Symbol.toStringTag,{value:"Module"})});
1
+ (function(g,I){typeof exports=="object"&&typeof module<"u"?I(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],I):(g=typeof globalThis<"u"?globalThis:g||self,I(g.ReactSticky={},g.jsxRuntime,g.React))})(this,function(g,I,n){"use strict";const N=n.createContext(null);function w(){return n.useContext(N)}const L="";function O({mode:l,children:h,className:k,...E}){const e=w(),m=n.useRef(null),C=n.useRef(null),[p,o]=n.useState(!1),d=n.useRef({fixedOffsetTop:(e==null?void 0:e.fixedOffsetTop)||0,isSticky:p,baseZIndex:(e==null?void 0:e.baseZIndex)||20});return d.current.baseZIndex=(e==null?void 0:e.baseZIndex)||20,d.current.fixedOffsetTop=(e==null?void 0:e.fixedOffsetTop)||0,d.current.isSticky=p,n.useEffect(()=>{const t=C.current,a=m.current,i=l||(e==null?void 0:e.mode);if(!e||!a||!t||i==="none"||!i||!e.register){o(!1);return}const v={el:a,update:(B,c,u,r,f)=>{if(!B||c>u)return d.current.isSticky&&o(!1),0;const s=t.getBoundingClientRect().height;let S=u;if(i==="replace"&&typeof r<"u"){const y=r-(u+s);if(y<0&&(S=u+y,y+s<0))return d.current.isSticky&&o(!1),0}return a.style.height=`${s}px`,d.current.isSticky||o(!0),t.style.top=`${S}px`,t.style.width=`${a.offsetWidth}px`,t.style.zIndex=`${d.current.baseZIndex+(i==="replace"?-f:f)}`,i==="replace"?0:s}};return e.register(v)},[e==null?void 0:e.mode,e==null?void 0:e.register,l]),n.useEffect(()=>{const t=m.current,a=C.current,i=e==null?void 0:e.updateStickyItemsHeight;if(!p||!t||!a||!i)return;const _=i(a.getBoundingClientRect().height);return()=>{b(t,["height"]),b(a,["top","z-index","width"]),_()}},[p,e==null?void 0:e.updateStickyItemsHeight]),I.jsx("div",{className:"oe-sticky-item "+(k||""),...E,ref:m,children:I.jsx("div",{className:"oe-sticky-content "+(p?"is-sticky":""),ref:C,children:h})})}function b(l,h){h.forEach(k=>{l.style.removeProperty(k)})}function Z({children:l,offsetTop:h=0,baseZIndex:k,onStickyItemsHeightChange:E,defaultMode:e="replace",constraint:m,...C}){const p=n.useRef(null),o=n.useRef([]),d=n.useRef(null),t=n.useRef({fixedOffsetTop:h,defaultMode:e,stickyItemsHeight:0,onStickyItemsHeightChange:E,lastCanSticky:!1});t.current.onStickyItemsHeightChange=E;const a=n.useCallback(()=>{var c;return m==="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=p.current)==null?void 0:c.getBoundingClientRect())||{top:0,bottom:0,left:0,right:0,width:0,height:0,x:0,y:0,toJSON:()=>({})}},[m]),i=n.useCallback(()=>{const c=p.current;!c||d.current||(d.current=requestAnimationFrame(()=>{d.current=null;const u=a(),{fixedOffsetTop:r,stickyItemsHeight:f}=t.current;let s;if(m==="none"?s=!0:s=!(u.top>r||u.bottom<r),!s){t.current.lastCanSticky!==s&&(c.classList.toggle("can-sticky",!1),o.current.forEach(R=>R.update(!1,0,0,0,0)),t.current.lastCanSticky=s);return}t.current.lastCanSticky=s,f>0&&c.classList.toggle("can-sticky",!0);let S,y=0;S=r,y=u.bottom-(r+f),y>0&&(y=0);const x=o.current.map(R=>R.el.getBoundingClientRect().top);o.current.forEach((R,H)=>{S+=R.update(s,x[H],S+y,x[H+1],H)})}))},[a,m]),_=n.useCallback(c=>{const u=t.current.stickyItemsHeight+c;return t.current.stickyItemsHeight=u,setTimeout(()=>{var r,f;(f=(r=t.current).onStickyItemsHeightChange)==null||f.call(r,u)},0),()=>{const r=t.current.stickyItemsHeight-c;t.current.stickyItemsHeight=r,setTimeout(()=>{var f,s;(s=(f=t.current).onStickyItemsHeightChange)==null||s.call(f,r)},0)}},[]);n.useEffect(()=>{t.current.fixedOffsetTop=h,t.current.defaultMode=e,setTimeout(()=>{i()},10)},[h,e,i]);const v=n.useCallback(c=>(o.current.push(c),o.current=A(o.current),i(),()=>{o.current=o.current.filter(u=>u!==c),i()}),[i]);n.useEffect(()=>(window.addEventListener("scroll",i,{passive:!0}),window.addEventListener("resize",i,{passive:!0}),()=>{window.removeEventListener("scroll",i),window.removeEventListener("resize",i)}),[i]);const B=n.useMemo(()=>T(k),[k]);return I.jsx(N.Provider,{value:{register:v,baseZIndex:B,updateStickyItemsHeight:_,fixedOffsetTop:h,mode:e},children:I.jsx("div",{...C,ref:p,className:"oe-sticky-container",children:l})})}function T(l){return typeof l>"u"?200:Math.max(Number(l)||0,20)}function A(l){return l.sort((h,k)=>{const E=h.el.getBoundingClientRect(),e=k.el.getBoundingClientRect();return E.top-e.top})}g.StickyContainer=Z,g.StickyItem=O,Object.defineProperty(g,Symbol.toStringTag,{value:"Module"})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-sticky-kit",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
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": [
@@ -20,14 +20,6 @@
20
20
  },
21
21
  "homepage": "https://github.com/oe/react-sticky-kit#readme",
22
22
  "license": "MIT",
23
- "scripts": {
24
- "dev": "vite",
25
- "build": "vite build",
26
- "preview": "vite preview",
27
- "type-check": "tsc --noEmit",
28
- "lint": "eslint src demo --ext .ts,.tsx,.js,.jsx",
29
- "pub": "tsx ./scripts/publish.ts"
30
- },
31
23
  "files": [
32
24
  "dist"
33
25
  ],
@@ -36,9 +28,9 @@
36
28
  "types": "dist/index.d.ts",
37
29
  "exports": {
38
30
  ".": {
31
+ "types": "./dist/index.d.ts",
39
32
  "import": "./dist/react-sticky.es.js",
40
- "require": "./dist/react-sticky.umd.js",
41
- "types": "./dist/index.d.ts"
33
+ "require": "./dist/react-sticky.umd.js"
42
34
  },
43
35
  "./style": "./dist/style.css",
44
36
  "./dist/style.css": "./dist/style.css"
@@ -49,25 +41,44 @@
49
41
  },
50
42
  "devDependencies": {
51
43
  "@eslint/js": "^9.25.0",
44
+ "@testing-library/jest-dom": "^6.1.6",
45
+ "@testing-library/react": "^14.1.2",
46
+ "@testing-library/user-event": "^14.4.3",
52
47
  "@types/node": "^22.15.18",
53
48
  "@types/react": "^19.1.2",
54
49
  "@types/react-dom": "^19.1.2",
55
50
  "@typescript-eslint/eslint-plugin": "^8.32.1",
56
51
  "@typescript-eslint/parser": "^8.32.1",
57
52
  "@vitejs/plugin-react": "^4.0.0",
53
+ "@vitest/coverage-v8": "^3.1.3",
58
54
  "eslint": "^9.26.0",
59
55
  "eslint-config-prettier": "^10.1.5",
60
56
  "eslint-plugin-jsx-a11y": "^6.8.0",
61
57
  "eslint-plugin-react": "^7.33.2",
62
58
  "eslint-plugin-react-hooks": "^5.2.0",
63
59
  "globals": "^16.0.0",
60
+ "identity-obj-proxy": "^3.0.0",
61
+ "jsdom": "^26.1.0",
64
62
  "react": "^19.1.0",
65
63
  "react-dom": "^19.1.0",
66
64
  "sass": "^1.88.0",
65
+ "ts-node": "^10.9.2",
67
66
  "tsx": "^4.19.4",
68
67
  "typescript": "^5.0.0",
69
68
  "typescript-eslint": "^8.30.1",
70
69
  "vite": "^4.0.0",
71
- "vite-plugin-dts": "^4.5.4"
70
+ "vite-plugin-dts": "^4.5.4",
71
+ "vitest": "^3.1.3"
72
+ },
73
+ "scripts": {
74
+ "dev": "vite",
75
+ "build": "vite build",
76
+ "preview": "vite preview",
77
+ "type-check": "tsc --noEmit",
78
+ "lint": "eslint src demo --ext .ts,.tsx,.js,.jsx",
79
+ "test": "vitest run",
80
+ "test:watch": "vitest",
81
+ "coverage": "vitest run --coverage",
82
+ "pub": "tsx ./scripts/publish.ts"
72
83
  }
73
- }
84
+ }