UNPKG

9.84 kBJavaScriptView Raw
1// src/index.tsx
2import { useEffect, useState } from "react";
3import { jsx } from "react/jsx-runtime";
4var getScrollParent = (node) => {
5 let parent = node;
6 while (parent = parent.parentElement) {
7 const overflowYVal = getComputedStyle(parent, null).getPropertyValue("overflow-y");
8 if (parent === document.body)
9 return window;
10 if (overflowYVal === "auto" || overflowYVal === "scroll" || overflowYVal === "overlay") {
11 return parent;
12 }
13 }
14 return window;
15};
16var isOffsetElement = (el) => el.firstChild ? el.firstChild.offsetParent === el : true;
17var offsetTill = (node, target) => {
18 let current = node;
19 let offset = 0;
20 if (!isOffsetElement(target)) {
21 offset += node.offsetTop - target.offsetTop;
22 target = node.offsetParent;
23 offset += -node.offsetTop;
24 }
25 do {
26 offset += current.offsetTop;
27 current = current.offsetParent;
28 } while (current && current !== target);
29 return offset;
30};
31var getParentNode = (node) => {
32 let currentParent = node.parentElement;
33 while (currentParent) {
34 const style = getComputedStyle(currentParent, null);
35 if (style.getPropertyValue("display") !== "contents")
36 break;
37 currentParent = currentParent.parentElement;
38 }
39 return currentParent || window;
40};
41var stickyProp = null;
42if (typeof CSS !== "undefined" && CSS.supports) {
43 if (CSS.supports("position", "sticky"))
44 stickyProp = "sticky";
45 else if (CSS.supports("position", "-webkit-sticky"))
46 stickyProp = "-webkit-sticky";
47}
48var passiveArg = false;
49try {
50 const opts = Object.defineProperty({}, "passive", {
51 // eslint-disable-next-line getter-return
52 get() {
53 passiveArg = { passive: true };
54 }
55 });
56 const emptyHandler = () => {
57 };
58 window.addEventListener("testPassive", emptyHandler, opts);
59 window.removeEventListener("testPassive", emptyHandler, opts);
60} catch (e) {
61}
62var getDimensions = (opts) => {
63 const { el, onChange, unsubs, measure } = opts;
64 if (el === window) {
65 const getRect = () => ({ top: 0, left: 0, height: window.innerHeight, width: window.innerWidth });
66 const mResult = measure(getRect());
67 const handler = () => {
68 Object.assign(mResult, measure(getRect()));
69 onChange();
70 };
71 window.addEventListener("resize", handler, passiveArg);
72 unsubs.push(() => window.removeEventListener("resize", handler));
73 return mResult;
74 } else {
75 const mResult = measure(el.getBoundingClientRect());
76 const handler = () => {
77 Object.assign(mResult, measure(el.getBoundingClientRect()));
78 onChange();
79 };
80 const ro = new ResizeObserver(handler);
81 ro.observe(el);
82 unsubs.push(() => ro.disconnect());
83 return mResult;
84 }
85};
86var getVerticalPadding = (node) => {
87 const computedParentStyle = getComputedStyle(node, null);
88 const parentPaddingTop = parseInt(computedParentStyle.getPropertyValue("padding-top"), 10);
89 const parentPaddingBottom = parseInt(computedParentStyle.getPropertyValue("padding-bottom"), 10);
90 return { top: parentPaddingTop, bottom: parentPaddingBottom };
91};
92var setup = (node, unsubs, opts) => {
93 const { bottom, offsetBottom, offsetTop } = opts;
94 const scrollPane = getScrollParent(node);
95 let isScheduled = false;
96 const scheduleOnLayout = () => {
97 if (!isScheduled) {
98 requestAnimationFrame(() => {
99 const nextMode = onLayout();
100 if (nextMode !== mode)
101 changeMode(nextMode);
102 isScheduled = false;
103 });
104 }
105 isScheduled = true;
106 };
107 let latestScrollY = scrollPane === window ? window.scrollY : scrollPane.scrollTop;
108 const isBoxTooLow = (scrollY) => {
109 const { offsetTop: scrollPaneOffset, height: viewPortHeight } = scrollPaneDims;
110 const { naturalTop } = parentDims;
111 const { height: nodeHeight } = nodeDims;
112 if (scrollY + scrollPaneOffset + viewPortHeight >= naturalTop + nodeHeight + relativeOffset + offsetBottom) {
113 return true;
114 }
115 return false;
116 };
117 const onLayout = () => {
118 const { height: viewPortHeight } = scrollPaneDims;
119 const { height: nodeHeight } = nodeDims;
120 if (nodeHeight + offsetTop + offsetBottom <= viewPortHeight) {
121 return 3 /* small */;
122 } else {
123 if (isBoxTooLow(latestScrollY)) {
124 return 1 /* stickyBottom */;
125 } else {
126 return 2 /* relative */;
127 }
128 }
129 };
130 const scrollPaneIsOffsetEl = scrollPane !== window && isOffsetElement(scrollPane);
131 const scrollPaneDims = getDimensions({
132 el: scrollPane,
133 onChange: scheduleOnLayout,
134 unsubs,
135 measure: ({ height, top }) => ({
136 height,
137 offsetTop: scrollPaneIsOffsetEl ? top : 0
138 })
139 });
140 const parentNode = getParentNode(node);
141 const parentPaddings = parentNode === window ? { top: 0, bottom: 0 } : getVerticalPadding(parentNode);
142 const parentDims = getDimensions({
143 el: parentNode,
144 onChange: scheduleOnLayout,
145 unsubs,
146 measure: ({ height }) => ({
147 height: height - parentPaddings.top - parentPaddings.bottom,
148 naturalTop: parentNode === window ? 0 : offsetTill(parentNode, scrollPane) + parentPaddings.top + scrollPaneDims.offsetTop
149 })
150 });
151 const nodeDims = getDimensions({
152 el: node,
153 onChange: scheduleOnLayout,
154 unsubs,
155 measure: ({ height }) => ({ height })
156 });
157 let relativeOffset = 0;
158 let mode = onLayout();
159 const changeMode = (newMode) => {
160 const prevMode = mode;
161 mode = newMode;
162 if (prevMode === 2 /* relative */)
163 relativeOffset = -1;
164 if (newMode === 3 /* small */) {
165 node.style.position = stickyProp;
166 if (bottom) {
167 node.style.bottom = `${offsetBottom}px`;
168 } else {
169 node.style.top = `${offsetTop}px`;
170 }
171 return;
172 }
173 const { height: viewPortHeight, offsetTop: scrollPaneOffset } = scrollPaneDims;
174 const { height: parentHeight, naturalTop } = parentDims;
175 const { height: nodeHeight } = nodeDims;
176 if (newMode === 2 /* relative */) {
177 node.style.position = "relative";
178 relativeOffset = prevMode === 0 /* stickyTop */ ? Math.max(0, scrollPaneOffset + latestScrollY - naturalTop + offsetTop) : Math.max(
179 0,
180 scrollPaneOffset + latestScrollY + viewPortHeight - (naturalTop + nodeHeight + offsetBottom)
181 );
182 if (bottom) {
183 const nextBottom = Math.max(0, parentHeight - nodeHeight - relativeOffset);
184 node.style.bottom = `${nextBottom}px`;
185 } else {
186 node.style.top = `${relativeOffset}px`;
187 }
188 } else {
189 node.style.position = stickyProp;
190 if (newMode === 1 /* stickyBottom */) {
191 if (bottom) {
192 node.style.bottom = `${offsetBottom}px`;
193 } else {
194 node.style.top = `${viewPortHeight - nodeHeight - offsetBottom}px`;
195 }
196 } else {
197 if (bottom) {
198 node.style.bottom = `${viewPortHeight - nodeHeight - offsetBottom}px`;
199 } else {
200 node.style.top = `${offsetTop}px`;
201 }
202 }
203 }
204 };
205 changeMode(mode);
206 const onScroll = (scrollY) => {
207 if (scrollY === latestScrollY)
208 return;
209 const scrollDelta = scrollY - latestScrollY;
210 latestScrollY = scrollY;
211 if (mode === 3 /* small */)
212 return;
213 const { offsetTop: scrollPaneOffset, height: viewPortHeight } = scrollPaneDims;
214 const { naturalTop, height: parentHeight } = parentDims;
215 const { height: nodeHeight } = nodeDims;
216 if (scrollDelta > 0) {
217 if (mode === 0 /* stickyTop */) {
218 if (scrollY + scrollPaneOffset + offsetTop > naturalTop) {
219 const topOffset = Math.max(0, scrollPaneOffset + latestScrollY - naturalTop + offsetTop);
220 if (scrollY + scrollPaneOffset + viewPortHeight <= naturalTop + nodeHeight + topOffset + offsetBottom) {
221 changeMode(2 /* relative */);
222 } else {
223 changeMode(1 /* stickyBottom */);
224 }
225 }
226 } else if (mode === 2 /* relative */) {
227 if (isBoxTooLow(scrollY))
228 changeMode(1 /* stickyBottom */);
229 }
230 } else {
231 if (mode === 1 /* stickyBottom */) {
232 if (scrollPaneOffset + scrollY + viewPortHeight < naturalTop + parentHeight + offsetBottom) {
233 const bottomOffset = Math.max(
234 0,
235 scrollPaneOffset + latestScrollY + viewPortHeight - (naturalTop + nodeHeight + offsetBottom)
236 );
237 if (scrollPaneOffset + scrollY + offsetTop >= naturalTop + bottomOffset) {
238 changeMode(2 /* relative */);
239 } else {
240 changeMode(0 /* stickyTop */);
241 }
242 }
243 } else if (mode === 2 /* relative */) {
244 if (scrollPaneOffset + scrollY + offsetTop < naturalTop + relativeOffset) {
245 changeMode(0 /* stickyTop */);
246 }
247 }
248 }
249 };
250 const handleScroll = scrollPane === window ? () => onScroll(window.scrollY) : () => onScroll(scrollPane.scrollTop);
251 scrollPane.addEventListener("scroll", handleScroll, passiveArg);
252 scrollPane.addEventListener("mousewheel", handleScroll, passiveArg);
253 unsubs.push(
254 () => scrollPane.removeEventListener("scroll", handleScroll),
255 () => scrollPane.removeEventListener("mousewheel", handleScroll)
256 );
257};
258var useStickyBox = ({
259 offsetTop = 0,
260 offsetBottom = 0,
261 bottom = false
262} = {}) => {
263 const [node, setNode] = useState(null);
264 useEffect(() => {
265 if (!node || !stickyProp)
266 return;
267 const unsubs = [];
268 setup(node, unsubs, { offsetBottom, offsetTop, bottom });
269 return () => {
270 unsubs.forEach((fn) => fn());
271 };
272 }, [node, offsetBottom, offsetTop, bottom]);
273 return setNode;
274};
275var StickyBox = (props) => {
276 const { offsetTop, offsetBottom, bottom, children, className, style } = props;
277 const ref = useStickyBox({ offsetTop, offsetBottom, bottom });
278 return /* @__PURE__ */ jsx("div", { className, style, ref, children });
279};
280var src_default = StickyBox;
281export {
282 src_default as default,
283 useStickyBox
284};