@usels/core 0.0.1

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.
Files changed (341) hide show
  1. package/README.md +21 -0
  2. package/dist/browser/useEventListener/index.d.mts +56 -0
  3. package/dist/browser/useEventListener/index.d.ts +56 -0
  4. package/dist/browser/useEventListener/index.js +112 -0
  5. package/dist/browser/useEventListener/index.js.map +1 -0
  6. package/dist/browser/useEventListener/index.mjs +88 -0
  7. package/dist/browser/useEventListener/index.mjs.map +1 -0
  8. package/dist/browser/useMediaQuery/demo.d.mts +5 -0
  9. package/dist/browser/useMediaQuery/demo.d.ts +5 -0
  10. package/dist/browser/useMediaQuery/demo.js +83 -0
  11. package/dist/browser/useMediaQuery/demo.js.map +1 -0
  12. package/dist/browser/useMediaQuery/demo.mjs +63 -0
  13. package/dist/browser/useMediaQuery/demo.mjs.map +1 -0
  14. package/dist/browser/useMediaQuery/index.d.mts +11 -0
  15. package/dist/browser/useMediaQuery/index.d.ts +11 -0
  16. package/dist/browser/useMediaQuery/index.js +89 -0
  17. package/dist/browser/useMediaQuery/index.js.map +1 -0
  18. package/dist/browser/useMediaQuery/index.mjs +64 -0
  19. package/dist/browser/useMediaQuery/index.mjs.map +1 -0
  20. package/dist/components/Auto/index.d.mts +33 -0
  21. package/dist/components/Auto/index.d.ts +33 -0
  22. package/dist/components/Auto/index.js +66 -0
  23. package/dist/components/Auto/index.js.map +1 -0
  24. package/dist/components/Auto/index.mjs +34 -0
  25. package/dist/components/Auto/index.mjs.map +1 -0
  26. package/dist/elements/useDocumentVisibility/demo.d.mts +5 -0
  27. package/dist/elements/useDocumentVisibility/demo.d.ts +5 -0
  28. package/dist/elements/useDocumentVisibility/demo.js +130 -0
  29. package/dist/elements/useDocumentVisibility/demo.js.map +1 -0
  30. package/dist/elements/useDocumentVisibility/demo.mjs +114 -0
  31. package/dist/elements/useDocumentVisibility/demo.mjs.map +1 -0
  32. package/dist/elements/useDocumentVisibility/index.d.mts +5 -0
  33. package/dist/elements/useDocumentVisibility/index.d.ts +5 -0
  34. package/dist/elements/useDocumentVisibility/index.js +45 -0
  35. package/dist/elements/useDocumentVisibility/index.js.map +1 -0
  36. package/dist/elements/useDocumentVisibility/index.mjs +21 -0
  37. package/dist/elements/useDocumentVisibility/index.mjs.map +1 -0
  38. package/dist/elements/useElementBounding/demo.d.mts +5 -0
  39. package/dist/elements/useElementBounding/demo.d.ts +5 -0
  40. package/dist/elements/useElementBounding/demo.js +87 -0
  41. package/dist/elements/useElementBounding/demo.js.map +1 -0
  42. package/dist/elements/useElementBounding/demo.mjs +67 -0
  43. package/dist/elements/useElementBounding/demo.mjs.map +1 -0
  44. package/dist/elements/useElementBounding/index.d.mts +46 -0
  45. package/dist/elements/useElementBounding/index.d.ts +46 -0
  46. package/dist/elements/useElementBounding/index.js +122 -0
  47. package/dist/elements/useElementBounding/index.js.map +1 -0
  48. package/dist/elements/useElementBounding/index.mjs +98 -0
  49. package/dist/elements/useElementBounding/index.mjs.map +1 -0
  50. package/dist/elements/useElementSize/demo.d.mts +5 -0
  51. package/dist/elements/useElementSize/demo.d.ts +5 -0
  52. package/dist/elements/useElementSize/demo.js +83 -0
  53. package/dist/elements/useElementSize/demo.js.map +1 -0
  54. package/dist/elements/useElementSize/demo.mjs +63 -0
  55. package/dist/elements/useElementSize/demo.mjs.map +1 -0
  56. package/dist/elements/useElementSize/index.d.mts +34 -0
  57. package/dist/elements/useElementSize/index.d.ts +34 -0
  58. package/dist/elements/useElementSize/index.js +85 -0
  59. package/dist/elements/useElementSize/index.js.map +1 -0
  60. package/dist/elements/useElementSize/index.mjs +61 -0
  61. package/dist/elements/useElementSize/index.mjs.map +1 -0
  62. package/dist/elements/useElementVisibility/demo.d.mts +5 -0
  63. package/dist/elements/useElementVisibility/demo.d.ts +5 -0
  64. package/dist/elements/useElementVisibility/demo.js +110 -0
  65. package/dist/elements/useElementVisibility/demo.js.map +1 -0
  66. package/dist/elements/useElementVisibility/demo.mjs +90 -0
  67. package/dist/elements/useElementVisibility/demo.mjs.map +1 -0
  68. package/dist/elements/useElementVisibility/index.d.mts +43 -0
  69. package/dist/elements/useElementVisibility/index.d.ts +43 -0
  70. package/dist/elements/useElementVisibility/index.js +58 -0
  71. package/dist/elements/useElementVisibility/index.js.map +1 -0
  72. package/dist/elements/useElementVisibility/index.mjs +34 -0
  73. package/dist/elements/useElementVisibility/index.mjs.map +1 -0
  74. package/dist/elements/useIntersectionObserver/demo.d.mts +5 -0
  75. package/dist/elements/useIntersectionObserver/demo.d.ts +5 -0
  76. package/dist/elements/useIntersectionObserver/demo.js +173 -0
  77. package/dist/elements/useIntersectionObserver/demo.js.map +1 -0
  78. package/dist/elements/useIntersectionObserver/demo.mjs +153 -0
  79. package/dist/elements/useIntersectionObserver/demo.mjs.map +1 -0
  80. package/dist/elements/useIntersectionObserver/index.d.mts +47 -0
  81. package/dist/elements/useIntersectionObserver/index.d.ts +47 -0
  82. package/dist/elements/useIntersectionObserver/index.js +111 -0
  83. package/dist/elements/useIntersectionObserver/index.js.map +1 -0
  84. package/dist/elements/useIntersectionObserver/index.mjs +87 -0
  85. package/dist/elements/useIntersectionObserver/index.mjs.map +1 -0
  86. package/dist/elements/useMouseInElement/demo.d.mts +5 -0
  87. package/dist/elements/useMouseInElement/demo.d.ts +5 -0
  88. package/dist/elements/useMouseInElement/demo.js +104 -0
  89. package/dist/elements/useMouseInElement/demo.js.map +1 -0
  90. package/dist/elements/useMouseInElement/demo.mjs +84 -0
  91. package/dist/elements/useMouseInElement/demo.mjs.map +1 -0
  92. package/dist/elements/useMouseInElement/index.d.mts +56 -0
  93. package/dist/elements/useMouseInElement/index.d.ts +56 -0
  94. package/dist/elements/useMouseInElement/index.js +148 -0
  95. package/dist/elements/useMouseInElement/index.js.map +1 -0
  96. package/dist/elements/useMouseInElement/index.mjs +124 -0
  97. package/dist/elements/useMouseInElement/index.mjs.map +1 -0
  98. package/dist/elements/useMutationObserver/demo.d.mts +5 -0
  99. package/dist/elements/useMutationObserver/demo.d.ts +5 -0
  100. package/dist/elements/useMutationObserver/demo.js +240 -0
  101. package/dist/elements/useMutationObserver/demo.js.map +1 -0
  102. package/dist/elements/useMutationObserver/demo.mjs +220 -0
  103. package/dist/elements/useMutationObserver/demo.mjs.map +1 -0
  104. package/dist/elements/useMutationObserver/index.d.mts +15 -0
  105. package/dist/elements/useMutationObserver/index.d.ts +15 -0
  106. package/dist/elements/useMutationObserver/index.js +69 -0
  107. package/dist/elements/useMutationObserver/index.js.map +1 -0
  108. package/dist/elements/useMutationObserver/index.mjs +45 -0
  109. package/dist/elements/useMutationObserver/index.mjs.map +1 -0
  110. package/dist/elements/useParentElement/demo.d.mts +5 -0
  111. package/dist/elements/useParentElement/demo.d.ts +5 -0
  112. package/dist/elements/useParentElement/demo.js +132 -0
  113. package/dist/elements/useParentElement/demo.js.map +1 -0
  114. package/dist/elements/useParentElement/demo.mjs +112 -0
  115. package/dist/elements/useParentElement/demo.mjs.map +1 -0
  116. package/dist/elements/useParentElement/index.d.mts +7 -0
  117. package/dist/elements/useParentElement/index.d.ts +7 -0
  118. package/dist/elements/useParentElement/index.js +47 -0
  119. package/dist/elements/useParentElement/index.js.map +1 -0
  120. package/dist/elements/useParentElement/index.mjs +23 -0
  121. package/dist/elements/useParentElement/index.mjs.map +1 -0
  122. package/dist/elements/useRef$/index.js +89 -0
  123. package/dist/elements/useRef$/index.js.map +1 -0
  124. package/dist/elements/useRef$/index.mjs +62 -0
  125. package/dist/elements/useRef$/index.mjs.map +1 -0
  126. package/dist/elements/useRef_/index.d.mts +60 -0
  127. package/dist/elements/useRef_/index.d.ts +60 -0
  128. package/dist/elements/useResizeObserver/demo.d.mts +5 -0
  129. package/dist/elements/useResizeObserver/demo.d.ts +5 -0
  130. package/dist/elements/useResizeObserver/demo.js +90 -0
  131. package/dist/elements/useResizeObserver/demo.js.map +1 -0
  132. package/dist/elements/useResizeObserver/demo.mjs +70 -0
  133. package/dist/elements/useResizeObserver/demo.mjs.map +1 -0
  134. package/dist/elements/useResizeObserver/index.d.mts +36 -0
  135. package/dist/elements/useResizeObserver/index.d.ts +36 -0
  136. package/dist/elements/useResizeObserver/index.js +74 -0
  137. package/dist/elements/useResizeObserver/index.js.map +1 -0
  138. package/dist/elements/useResizeObserver/index.mjs +49 -0
  139. package/dist/elements/useResizeObserver/index.mjs.map +1 -0
  140. package/dist/elements/useWindowFocus/demo.d.mts +5 -0
  141. package/dist/elements/useWindowFocus/demo.d.ts +5 -0
  142. package/dist/elements/useWindowFocus/demo.js +104 -0
  143. package/dist/elements/useWindowFocus/demo.js.map +1 -0
  144. package/dist/elements/useWindowFocus/demo.mjs +84 -0
  145. package/dist/elements/useWindowFocus/demo.mjs.map +1 -0
  146. package/dist/elements/useWindowFocus/index.d.mts +5 -0
  147. package/dist/elements/useWindowFocus/index.d.ts +5 -0
  148. package/dist/elements/useWindowFocus/index.js +42 -0
  149. package/dist/elements/useWindowFocus/index.js.map +1 -0
  150. package/dist/elements/useWindowFocus/index.mjs +18 -0
  151. package/dist/elements/useWindowFocus/index.mjs.map +1 -0
  152. package/dist/elements/useWindowSize/demo.d.mts +5 -0
  153. package/dist/elements/useWindowSize/demo.d.ts +5 -0
  154. package/dist/elements/useWindowSize/demo.js +79 -0
  155. package/dist/elements/useWindowSize/demo.js.map +1 -0
  156. package/dist/elements/useWindowSize/demo.mjs +59 -0
  157. package/dist/elements/useWindowSize/demo.mjs.map +1 -0
  158. package/dist/elements/useWindowSize/index.d.mts +17 -0
  159. package/dist/elements/useWindowSize/index.d.ts +17 -0
  160. package/dist/elements/useWindowSize/index.js +96 -0
  161. package/dist/elements/useWindowSize/index.js.map +1 -0
  162. package/dist/elements/useWindowSize/index.mjs +76 -0
  163. package/dist/elements/useWindowSize/index.mjs.map +1 -0
  164. package/dist/function/get/index.d.mts +45 -0
  165. package/dist/function/get/index.d.ts +45 -0
  166. package/dist/function/get/index.js +39 -0
  167. package/dist/function/get/index.js.map +1 -0
  168. package/dist/function/get/index.mjs +15 -0
  169. package/dist/function/get/index.mjs.map +1 -0
  170. package/dist/function/peek/index.d.mts +46 -0
  171. package/dist/function/peek/index.d.ts +46 -0
  172. package/dist/function/peek/index.js +39 -0
  173. package/dist/function/peek/index.js.map +1 -0
  174. package/dist/function/peek/index.mjs +15 -0
  175. package/dist/function/peek/index.mjs.map +1 -0
  176. package/dist/function/useMayObservableOptions/index.d.mts +59 -0
  177. package/dist/function/useMayObservableOptions/index.d.ts +59 -0
  178. package/dist/function/useMayObservableOptions/index.js +109 -0
  179. package/dist/function/useMayObservableOptions/index.js.map +1 -0
  180. package/dist/function/useMayObservableOptions/index.mjs +88 -0
  181. package/dist/function/useMayObservableOptions/index.mjs.map +1 -0
  182. package/dist/function/useSupported/index.d.mts +6 -0
  183. package/dist/function/useSupported/index.d.ts +6 -0
  184. package/dist/function/useSupported/index.js +37 -0
  185. package/dist/function/useSupported/index.js.map +1 -0
  186. package/dist/function/useSupported/index.mjs +13 -0
  187. package/dist/function/useSupported/index.mjs.map +1 -0
  188. package/dist/function/useWhenMounted/index.d.mts +6 -0
  189. package/dist/function/useWhenMounted/index.d.ts +6 -0
  190. package/dist/function/useWhenMounted/index.js +37 -0
  191. package/dist/function/useWhenMounted/index.js.map +1 -0
  192. package/dist/function/useWhenMounted/index.mjs +13 -0
  193. package/dist/function/useWhenMounted/index.mjs.map +1 -0
  194. package/dist/index.d.mts +24 -0
  195. package/dist/index.d.ts +24 -0
  196. package/dist/index.js +63 -0
  197. package/dist/index.js.map +1 -0
  198. package/dist/index.mjs +22 -0
  199. package/dist/index.mjs.map +1 -0
  200. package/dist/sensors/useScroll/demo.d.mts +5 -0
  201. package/dist/sensors/useScroll/demo.d.ts +5 -0
  202. package/dist/sensors/useScroll/demo.js +122 -0
  203. package/dist/sensors/useScroll/demo.js.map +1 -0
  204. package/dist/sensors/useScroll/demo.mjs +102 -0
  205. package/dist/sensors/useScroll/demo.mjs.map +1 -0
  206. package/dist/sensors/useScroll/index.d.mts +42 -0
  207. package/dist/sensors/useScroll/index.d.ts +42 -0
  208. package/dist/sensors/useScroll/index.js +149 -0
  209. package/dist/sensors/useScroll/index.js.map +1 -0
  210. package/dist/sensors/useScroll/index.mjs +125 -0
  211. package/dist/sensors/useScroll/index.mjs.map +1 -0
  212. package/dist/sensors/useWindowScroll/demo.d.mts +5 -0
  213. package/dist/sensors/useWindowScroll/demo.d.ts +5 -0
  214. package/dist/sensors/useWindowScroll/demo.js +85 -0
  215. package/dist/sensors/useWindowScroll/demo.js.map +1 -0
  216. package/dist/sensors/useWindowScroll/demo.mjs +65 -0
  217. package/dist/sensors/useWindowScroll/demo.mjs.map +1 -0
  218. package/dist/sensors/useWindowScroll/index.d.mts +9 -0
  219. package/dist/sensors/useWindowScroll/index.d.ts +9 -0
  220. package/dist/sensors/useWindowScroll/index.js +36 -0
  221. package/dist/sensors/useWindowScroll/index.js.map +1 -0
  222. package/dist/sensors/useWindowScroll/index.mjs +12 -0
  223. package/dist/sensors/useWindowScroll/index.mjs.map +1 -0
  224. package/dist/shared/configurable.d.mts +21 -0
  225. package/dist/shared/configurable.d.ts +21 -0
  226. package/dist/shared/configurable.js +39 -0
  227. package/dist/shared/configurable.js.map +1 -0
  228. package/dist/shared/configurable.mjs +12 -0
  229. package/dist/shared/configurable.mjs.map +1 -0
  230. package/dist/shared/index.d.mts +4 -0
  231. package/dist/shared/index.d.ts +4 -0
  232. package/dist/shared/index.js +31 -0
  233. package/dist/shared/index.js.map +1 -0
  234. package/dist/shared/index.mjs +7 -0
  235. package/dist/shared/index.mjs.map +1 -0
  236. package/dist/shared/normalizeTargets/index.d.mts +21 -0
  237. package/dist/shared/normalizeTargets/index.d.ts +21 -0
  238. package/dist/shared/normalizeTargets/index.js +36 -0
  239. package/dist/shared/normalizeTargets/index.js.map +1 -0
  240. package/dist/shared/normalizeTargets/index.mjs +12 -0
  241. package/dist/shared/normalizeTargets/index.mjs.map +1 -0
  242. package/dist/shared/utils.d.mts +15 -0
  243. package/dist/shared/utils.d.ts +15 -0
  244. package/dist/shared/utils.js +87 -0
  245. package/dist/shared/utils.js.map +1 -0
  246. package/dist/shared/utils.mjs +52 -0
  247. package/dist/shared/utils.mjs.map +1 -0
  248. package/dist/types.d.mts +52 -0
  249. package/dist/types.d.ts +52 -0
  250. package/dist/types.js +17 -0
  251. package/dist/types.js.map +1 -0
  252. package/dist/types.mjs +1 -0
  253. package/dist/types.mjs.map +1 -0
  254. package/package.json +54 -0
  255. package/src/browser/useEventListener/index.md +109 -0
  256. package/src/browser/useEventListener/index.spec.ts +611 -0
  257. package/src/browser/useEventListener/index.ts +242 -0
  258. package/src/browser/useMediaQuery/demo.tsx +63 -0
  259. package/src/browser/useMediaQuery/index.md +43 -0
  260. package/src/browser/useMediaQuery/index.spec.ts +267 -0
  261. package/src/browser/useMediaQuery/index.ts +96 -0
  262. package/src/components/Auto/index.tsx +65 -0
  263. package/src/elements/useDocumentVisibility/demo.tsx +111 -0
  264. package/src/elements/useDocumentVisibility/index.md +54 -0
  265. package/src/elements/useDocumentVisibility/index.spec.ts +114 -0
  266. package/src/elements/useDocumentVisibility/index.ts +26 -0
  267. package/src/elements/useElementBounding/demo.tsx +68 -0
  268. package/src/elements/useElementBounding/index.md +64 -0
  269. package/src/elements/useElementBounding/index.ts +159 -0
  270. package/src/elements/useElementSize/demo.tsx +53 -0
  271. package/src/elements/useElementSize/index.md +65 -0
  272. package/src/elements/useElementSize/index.spec.ts +295 -0
  273. package/src/elements/useElementSize/index.ts +100 -0
  274. package/src/elements/useElementVisibility/deep-observable-pattern.spec.ts +453 -0
  275. package/src/elements/useElementVisibility/demo.tsx +97 -0
  276. package/src/elements/useElementVisibility/index.md +98 -0
  277. package/src/elements/useElementVisibility/index.spec.ts +227 -0
  278. package/src/elements/useElementVisibility/index.ts +78 -0
  279. package/src/elements/useIntersectionObserver/demo.tsx +180 -0
  280. package/src/elements/useIntersectionObserver/index.md +99 -0
  281. package/src/elements/useIntersectionObserver/index.spec.ts +482 -0
  282. package/src/elements/useIntersectionObserver/index.ts +149 -0
  283. package/src/elements/useMouseInElement/demo.tsx +88 -0
  284. package/src/elements/useMouseInElement/index.md +76 -0
  285. package/src/elements/useMouseInElement/index.spec.ts +398 -0
  286. package/src/elements/useMouseInElement/index.ts +209 -0
  287. package/src/elements/useMutationObserver/demo.tsx +270 -0
  288. package/src/elements/useMutationObserver/index.md +99 -0
  289. package/src/elements/useMutationObserver/index.spec.ts +421 -0
  290. package/src/elements/useMutationObserver/index.ts +66 -0
  291. package/src/elements/useParentElement/demo.tsx +120 -0
  292. package/src/elements/useParentElement/index.md +67 -0
  293. package/src/elements/useParentElement/index.spec.ts +208 -0
  294. package/src/elements/useParentElement/index.ts +35 -0
  295. package/src/elements/useRef$/index.md +62 -0
  296. package/src/elements/useRef$/index.spec.ts +205 -0
  297. package/src/elements/useRef$/index.ts +137 -0
  298. package/src/elements/useRef$/useImperativeHandle.spec.ts +339 -0
  299. package/src/elements/useResizeObserver/demo.tsx +62 -0
  300. package/src/elements/useResizeObserver/index.md +51 -0
  301. package/src/elements/useResizeObserver/index.spec.ts +312 -0
  302. package/src/elements/useResizeObserver/index.ts +106 -0
  303. package/src/elements/useWindowFocus/demo.tsx +79 -0
  304. package/src/elements/useWindowFocus/index.md +38 -0
  305. package/src/elements/useWindowFocus/index.spec.ts +103 -0
  306. package/src/elements/useWindowFocus/index.ts +21 -0
  307. package/src/elements/useWindowSize/demo.tsx +51 -0
  308. package/src/elements/useWindowSize/index.md +55 -0
  309. package/src/elements/useWindowSize/index.spec.ts +310 -0
  310. package/src/elements/useWindowSize/index.ts +107 -0
  311. package/src/function/get/index.md +25 -0
  312. package/src/function/get/index.spec.ts +87 -0
  313. package/src/function/get/index.ts +70 -0
  314. package/src/function/peek/index.spec.ts +97 -0
  315. package/src/function/peek/index.ts +69 -0
  316. package/src/function/useMayObservableOptions/index.spec.ts +521 -0
  317. package/src/function/useMayObservableOptions/index.ts +173 -0
  318. package/src/function/useSupported/index.md +43 -0
  319. package/src/function/useSupported/index.spec.ts +116 -0
  320. package/src/function/useSupported/index.ts +14 -0
  321. package/src/function/useWhenMounted/index.md +25 -0
  322. package/src/function/useWhenMounted/index.spec.ts +120 -0
  323. package/src/function/useWhenMounted/index.ts +16 -0
  324. package/src/index.ts +25 -0
  325. package/src/sensors/useScroll/demo.tsx +103 -0
  326. package/src/sensors/useScroll/index.md +117 -0
  327. package/src/sensors/useScroll/index.spec.ts +678 -0
  328. package/src/sensors/useScroll/index.ts +201 -0
  329. package/src/sensors/useWindowScroll/demo.tsx +78 -0
  330. package/src/sensors/useWindowScroll/index.md +98 -0
  331. package/src/sensors/useWindowScroll/index.spec.ts +69 -0
  332. package/src/sensors/useWindowScroll/index.ts +11 -0
  333. package/src/shared/configurable.ts +35 -0
  334. package/src/shared/index.ts +4 -0
  335. package/src/shared/normalizeTargets/index.spec.ts +76 -0
  336. package/src/shared/normalizeTargets/index.ts +27 -0
  337. package/src/shared/utils.ts +67 -0
  338. package/src/types.ts +56 -0
  339. package/tsconfig.json +9 -0
  340. package/tsup.config.ts +10 -0
  341. package/vitest.config.ts +22 -0
@@ -0,0 +1,482 @@
1
+ // @vitest-environment jsdom
2
+ import { act, renderHook } from "@testing-library/react";
3
+ import { observable, ObservableHint } from "@legendapp/state";
4
+ import type { OpaqueObject } from "@legendapp/state";
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
6
+ import { useIntersectionObserver } from ".";
7
+ import { useRef$ } from "../useRef$";
8
+
9
+ const wrapEl = (el: Element) => observable<OpaqueObject<Element> | null>(ObservableHint.opaque(el));
10
+
11
+ const mockObserve = vi.fn();
12
+ const mockDisconnect = vi.fn();
13
+ let capturedCallback: IntersectionObserverCallback;
14
+
15
+ const MockIntersectionObserver = vi.fn(
16
+ (cb: IntersectionObserverCallback, init?: IntersectionObserverInit) => {
17
+ capturedCallback = cb;
18
+ void init; // captured for assertion via toHaveBeenCalledWith
19
+ return { observe: mockObserve, disconnect: mockDisconnect };
20
+ },
21
+ );
22
+
23
+ beforeEach(() => {
24
+ vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
25
+ mockObserve.mockClear();
26
+ mockDisconnect.mockClear();
27
+ MockIntersectionObserver.mockClear();
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.unstubAllGlobals();
32
+ });
33
+
34
+ describe("useIntersectionObserver()", () => {
35
+ it("creates observer and observes target on mount", () => {
36
+ const el = document.createElement("div");
37
+ renderHook(() => useIntersectionObserver(wrapEl(el),vi.fn()));
38
+
39
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
40
+ expect(mockObserve).toHaveBeenCalledWith(el);
41
+ });
42
+
43
+ it("isSupported is true when IntersectionObserver is available", () => {
44
+ const el = document.createElement("div");
45
+ const { result } = renderHook(() => useIntersectionObserver(wrapEl(el),vi.fn()));
46
+
47
+ expect(result.current.isSupported.get()).toBe(true);
48
+ });
49
+
50
+ it("isActive is true by default", () => {
51
+ const el = document.createElement("div");
52
+ const { result } = renderHook(() => useIntersectionObserver(wrapEl(el),vi.fn()));
53
+
54
+ expect(result.current.isActive.get()).toBe(true);
55
+ });
56
+
57
+ it("does not start observer when immediate is false", () => {
58
+ const el = document.createElement("div");
59
+ renderHook(() =>
60
+ useIntersectionObserver(wrapEl(el),vi.fn(), { immediate: false }),
61
+ );
62
+
63
+ expect(MockIntersectionObserver).not.toHaveBeenCalled();
64
+ });
65
+
66
+ it("isActive is false when immediate is false", () => {
67
+ const el = document.createElement("div");
68
+ const { result } = renderHook(() =>
69
+ useIntersectionObserver(wrapEl(el),vi.fn(), { immediate: false }),
70
+ );
71
+
72
+ expect(result.current.isActive.get()).toBe(false);
73
+ });
74
+
75
+ it("pause() disconnects observer and sets isActive to false", () => {
76
+ const el = document.createElement("div");
77
+ const { result } = renderHook(() => useIntersectionObserver(wrapEl(el),vi.fn()));
78
+
79
+ act(() => {
80
+ result.current.pause();
81
+ });
82
+
83
+ expect(mockDisconnect).toHaveBeenCalled();
84
+ expect(result.current.isActive.get()).toBe(false);
85
+ });
86
+
87
+ it("resume() restarts the observer after pause", () => {
88
+ const el = document.createElement("div");
89
+ const { result } = renderHook(() => useIntersectionObserver(wrapEl(el),vi.fn()));
90
+
91
+ act(() => result.current.pause());
92
+
93
+ mockObserve.mockClear();
94
+ MockIntersectionObserver.mockClear();
95
+
96
+ act(() => result.current.resume());
97
+
98
+ expect(result.current.isActive.get()).toBe(true);
99
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
100
+ expect(mockObserve).toHaveBeenCalledWith(el);
101
+ });
102
+
103
+ it("stop() permanently stops the observer", () => {
104
+ const el = document.createElement("div");
105
+ const { result } = renderHook(() => useIntersectionObserver(wrapEl(el),vi.fn()));
106
+
107
+ act(() => result.current.stop());
108
+
109
+ expect(mockDisconnect).toHaveBeenCalled();
110
+ expect(result.current.isActive.get()).toBe(false);
111
+ });
112
+
113
+ it("resume() has no effect after stop()", () => {
114
+ const el = document.createElement("div");
115
+ const { result } = renderHook(() => useIntersectionObserver(wrapEl(el),vi.fn()));
116
+
117
+ act(() => result.current.stop());
118
+ MockIntersectionObserver.mockClear();
119
+
120
+ act(() => result.current.resume());
121
+
122
+ expect(MockIntersectionObserver).not.toHaveBeenCalled();
123
+ });
124
+
125
+ it("observes multiple targets passed as array", () => {
126
+ const el1 = document.createElement("div");
127
+ const el2 = document.createElement("div");
128
+ renderHook(() => useIntersectionObserver([wrapEl(el1), wrapEl(el2)], vi.fn()));
129
+
130
+ expect(mockObserve).toHaveBeenCalledWith(el1);
131
+ expect(mockObserve).toHaveBeenCalledWith(el2);
132
+ });
133
+
134
+ it("works with Observable<OpaqueObject<Element>> target", () => {
135
+ const el = document.createElement("div");
136
+ renderHook(() => useIntersectionObserver(wrapEl(el), vi.fn()));
137
+
138
+ expect(mockObserve).toHaveBeenCalledWith(el);
139
+ });
140
+
141
+ it("skips null targets without throwing", () => {
142
+ const el$ = observable<OpaqueObject<Element> | null>(null);
143
+ expect(() =>
144
+ renderHook(() => useIntersectionObserver(el$, vi.fn())),
145
+ ).not.toThrow();
146
+ expect(mockObserve).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it("works with Ref$ target", () => {
150
+ const div = document.createElement("div");
151
+ const { result } = renderHook(() => {
152
+ const el$ = useRef$<Element>();
153
+ return { el$, io: useIntersectionObserver(el$, vi.fn()) };
154
+ });
155
+
156
+ // Before assignment — observe not yet called
157
+ expect(mockObserve).not.toHaveBeenCalled();
158
+
159
+ // Assign element via act after mount
160
+ act(() => result.current.el$(div));
161
+
162
+ // After assignment — observer must have been called with the element
163
+ expect(mockObserve).toHaveBeenCalledWith(div);
164
+ expect(result.current.io.isSupported.get()).toBe(true);
165
+ });
166
+
167
+ it("passes threshold option to IntersectionObserver", () => {
168
+ const el = document.createElement("div");
169
+ renderHook(() =>
170
+ useIntersectionObserver(wrapEl(el),vi.fn(), { threshold: 0.5 }),
171
+ );
172
+
173
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
174
+ expect.any(Function),
175
+ expect.objectContaining({ threshold: 0.5 }),
176
+ );
177
+ });
178
+
179
+ it("passes rootMargin option to IntersectionObserver", () => {
180
+ const el = document.createElement("div");
181
+ renderHook(() =>
182
+ useIntersectionObserver(wrapEl(el),vi.fn(), { rootMargin: "10px" }),
183
+ );
184
+
185
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
186
+ expect.any(Function),
187
+ expect.objectContaining({ rootMargin: "10px" }),
188
+ );
189
+ });
190
+
191
+ it("passes Observable rootMargin to IntersectionObserver", () => {
192
+ const el = document.createElement("div");
193
+ const rootMargin$ = observable("20px");
194
+ renderHook(() =>
195
+ useIntersectionObserver(wrapEl(el),vi.fn(), { rootMargin: rootMargin$ }),
196
+ );
197
+
198
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
199
+ expect.any(Function),
200
+ expect.objectContaining({ rootMargin: "20px" }),
201
+ );
202
+ });
203
+
204
+ it("reactively recreates observer when Observable rootMargin changes", () => {
205
+ const el = document.createElement("div");
206
+ const rootMargin$ = observable("0px");
207
+ renderHook(() =>
208
+ useIntersectionObserver(wrapEl(el),vi.fn(), { rootMargin: rootMargin$ }),
209
+ );
210
+
211
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
212
+ mockDisconnect.mockClear();
213
+ MockIntersectionObserver.mockClear();
214
+
215
+ act(() => {
216
+ rootMargin$.set("10px");
217
+ });
218
+
219
+ expect(mockDisconnect).toHaveBeenCalledTimes(1);
220
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
221
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
222
+ expect.any(Function),
223
+ expect.objectContaining({ rootMargin: "10px" }),
224
+ );
225
+ });
226
+
227
+ it("disconnects observer on unmount", () => {
228
+ const el = document.createElement("div");
229
+ const { unmount } = renderHook(() => useIntersectionObserver(wrapEl(el),vi.fn()));
230
+
231
+ unmount();
232
+
233
+ expect(mockDisconnect).toHaveBeenCalled();
234
+ });
235
+
236
+ it("isSupported is false when IntersectionObserver is not available", () => {
237
+ vi.stubGlobal("IntersectionObserver", undefined);
238
+ const el = document.createElement("div");
239
+ const { result } = renderHook(() => useIntersectionObserver(wrapEl(el),vi.fn()));
240
+
241
+ expect(result.current.isSupported.get()).toBe(false);
242
+ expect(MockIntersectionObserver).not.toHaveBeenCalled();
243
+ });
244
+
245
+ it("invokes callback when intersection fires", () => {
246
+ const el = document.createElement("div");
247
+ const cb = vi.fn();
248
+ renderHook(() => useIntersectionObserver(wrapEl(el),cb));
249
+
250
+ const entry = { isIntersecting: true } as IntersectionObserverEntry;
251
+ act(() => {
252
+ capturedCallback([entry], {} as IntersectionObserver);
253
+ });
254
+
255
+ expect(cb).toHaveBeenCalledWith([entry], expect.anything());
256
+ });
257
+
258
+ it("passes root option to IntersectionObserver", () => {
259
+ const el = document.createElement("div");
260
+ const root = document.createElement("div");
261
+ renderHook(() =>
262
+ useIntersectionObserver(wrapEl(el), vi.fn(), { root: wrapEl(root) }),
263
+ );
264
+
265
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
266
+ expect.any(Function),
267
+ expect.objectContaining({ root }),
268
+ );
269
+ });
270
+
271
+ it("passes Observable root to IntersectionObserver", () => {
272
+ const el = document.createElement("div");
273
+ const root = document.createElement("div");
274
+ const root$ = wrapEl(root);
275
+ renderHook(() =>
276
+ useIntersectionObserver(wrapEl(el), vi.fn(), { root: root$ }),
277
+ );
278
+
279
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
280
+ expect.any(Function),
281
+ expect.objectContaining({ root }),
282
+ );
283
+ });
284
+
285
+ it("reactively recreates observer when Observable root changes from null to element", () => {
286
+ const el = document.createElement("div");
287
+ const rootB = document.createElement("div");
288
+ // Start with null — @legendapp/state reliably tracks null→element transitions
289
+ const root$ = observable<OpaqueObject<Element> | null>(null);
290
+
291
+ renderHook(() =>
292
+ useIntersectionObserver(wrapEl(el), vi.fn(), { root: root$ }),
293
+ );
294
+
295
+ // root is null — observer must not be created yet
296
+ expect(MockIntersectionObserver).not.toHaveBeenCalled();
297
+ mockDisconnect.mockClear();
298
+ MockIntersectionObserver.mockClear();
299
+
300
+ act(() => {
301
+ root$.set(ObservableHint.opaque(rootB));
302
+ });
303
+
304
+ // no old observer to disconnect; new one created with rootB
305
+ expect(mockDisconnect).not.toHaveBeenCalled();
306
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
307
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
308
+ expect.any(Function),
309
+ expect.objectContaining({ root: rootB }),
310
+ );
311
+ });
312
+
313
+ it("delays setup until Ref$ root is mounted", () => {
314
+ const el = document.createElement("div");
315
+ const rootDiv = document.createElement("div");
316
+
317
+ const { result } = renderHook(() => {
318
+ const root$ = useRef$<HTMLElement>();
319
+ return { root$, io: useIntersectionObserver(wrapEl(el),vi.fn(), { root: root$ }) };
320
+ });
321
+
322
+ // root Ref$ is null — observer must not be created yet
323
+ expect(MockIntersectionObserver).not.toHaveBeenCalled();
324
+
325
+ // assign the root element
326
+ act(() => result.current.root$(rootDiv));
327
+
328
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
329
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
330
+ expect.any(Function),
331
+ expect.objectContaining({ root: rootDiv }),
332
+ );
333
+ });
334
+
335
+ it("reactively recreates observer when Ref$ root changes", () => {
336
+ const el = document.createElement("div");
337
+ const rootA = document.createElement("div");
338
+ const rootB = document.createElement("div");
339
+
340
+ const { result } = renderHook(() => {
341
+ const root$ = useRef$<HTMLElement>();
342
+ return { root$, io: useIntersectionObserver(wrapEl(el),vi.fn(), { root: root$ }) };
343
+ });
344
+
345
+ act(() => result.current.root$(rootA));
346
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
347
+
348
+ mockDisconnect.mockClear();
349
+ MockIntersectionObserver.mockClear();
350
+
351
+ act(() => result.current.root$(rootB));
352
+
353
+ expect(mockDisconnect).toHaveBeenCalledTimes(1);
354
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
355
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
356
+ expect.any(Function),
357
+ expect.objectContaining({ root: rootB }),
358
+ );
359
+ });
360
+
361
+ it("passes threshold array [0, 0.5, 1] to IntersectionObserver", () => {
362
+ const el = document.createElement("div");
363
+ renderHook(() =>
364
+ useIntersectionObserver(wrapEl(el),vi.fn(), { threshold: [0, 0.5, 1] }),
365
+ );
366
+
367
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
368
+ expect.any(Function),
369
+ expect.objectContaining({ threshold: [0, 0.5, 1] }),
370
+ );
371
+ });
372
+
373
+ it("pause → resume → stop sequence transitions states correctly", () => {
374
+ const el = document.createElement("div");
375
+ const { result } = renderHook(() => useIntersectionObserver(wrapEl(el),vi.fn()));
376
+
377
+ expect(result.current.isActive.get()).toBe(true);
378
+
379
+ // pause
380
+ act(() => result.current.pause());
381
+ expect(result.current.isActive.get()).toBe(false);
382
+ expect(mockDisconnect).toHaveBeenCalledTimes(1);
383
+
384
+ // resume
385
+ mockObserve.mockClear();
386
+ MockIntersectionObserver.mockClear();
387
+ act(() => result.current.resume());
388
+ expect(result.current.isActive.get()).toBe(true);
389
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
390
+ expect(mockObserve).toHaveBeenCalledWith(el);
391
+
392
+ // stop
393
+ mockDisconnect.mockClear();
394
+ act(() => result.current.stop());
395
+ expect(result.current.isActive.get()).toBe(false);
396
+ expect(mockDisconnect).toHaveBeenCalledTimes(1);
397
+
398
+ // resume after stop has no effect
399
+ MockIntersectionObserver.mockClear();
400
+ act(() => result.current.resume());
401
+ expect(MockIntersectionObserver).not.toHaveBeenCalled();
402
+ expect(result.current.isActive.get()).toBe(false);
403
+ });
404
+
405
+ it("recreates observer when Ref$ target changes to a different element", () => {
406
+ const elA = document.createElement("div");
407
+ const elB = document.createElement("div");
408
+
409
+ const { result } = renderHook(() => {
410
+ const el$ = useRef$<Element>();
411
+ return { el$, io: useIntersectionObserver(el$, vi.fn()) };
412
+ });
413
+
414
+ act(() => result.current.el$(elA));
415
+
416
+ expect(mockObserve).toHaveBeenCalledWith(elA);
417
+ mockObserve.mockClear();
418
+ mockDisconnect.mockClear();
419
+ MockIntersectionObserver.mockClear();
420
+
421
+ act(() => result.current.el$(elB));
422
+
423
+ expect(mockDisconnect).toHaveBeenCalledTimes(1);
424
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
425
+ expect(mockObserve).toHaveBeenCalledWith(elB);
426
+ });
427
+
428
+ it("multiple resume() calls after stop() are all ignored", () => {
429
+ const el = document.createElement("div");
430
+ const { result } = renderHook(() => useIntersectionObserver(wrapEl(el),vi.fn()));
431
+
432
+ act(() => result.current.stop());
433
+ MockIntersectionObserver.mockClear();
434
+
435
+ act(() => {
436
+ result.current.resume();
437
+ result.current.resume();
438
+ result.current.resume();
439
+ });
440
+
441
+ expect(MockIntersectionObserver).not.toHaveBeenCalled();
442
+ expect(result.current.isActive.get()).toBe(false);
443
+ });
444
+
445
+ it("pause() after unmount does not throw", () => {
446
+ const el = document.createElement("div");
447
+ const { result, unmount } = renderHook(() =>
448
+ useIntersectionObserver(wrapEl(el),vi.fn()),
449
+ );
450
+
451
+ unmount();
452
+ mockDisconnect.mockClear();
453
+
454
+ expect(() => act(() => result.current.pause())).not.toThrow();
455
+ // No additional disconnect after unmount cleanup
456
+ expect(mockDisconnect).not.toHaveBeenCalled();
457
+ });
458
+
459
+ it("resume() after unmount does not recreate observer", () => {
460
+ const el = document.createElement("div");
461
+ const { result, unmount } = renderHook(() =>
462
+ useIntersectionObserver(wrapEl(el),vi.fn()),
463
+ );
464
+
465
+ unmount();
466
+ MockIntersectionObserver.mockClear();
467
+
468
+ expect(() => act(() => result.current.resume())).not.toThrow();
469
+ expect(MockIntersectionObserver).not.toHaveBeenCalled();
470
+ });
471
+
472
+ it("stop() after unmount does not throw", () => {
473
+ const el = document.createElement("div");
474
+ const { result, unmount } = renderHook(() =>
475
+ useIntersectionObserver(wrapEl(el),vi.fn()),
476
+ );
477
+
478
+ unmount();
479
+
480
+ expect(() => act(() => result.current.stop())).not.toThrow();
481
+ });
482
+ });
@@ -0,0 +1,149 @@
1
+ import type { Observable } from "@legendapp/state";
2
+ import { useObservable, useObserveEffect } from "@legendapp/state/react";
3
+ import { useEffect, useRef } from "react";
4
+ import { useMayObservableOptions } from "../../function/useMayObservableOptions";
5
+ import type { DeepMaybeObservable, MaybeObservable } from "../../types";
6
+ import { isWindow } from "../../shared";
7
+ import type { MaybeElement } from "../useRef$";
8
+ import { normalizeTargets } from "../useResizeObserver";
9
+ import { get } from "../../function/get";
10
+
11
+ export interface UseIntersectionObserverOptions {
12
+ /** Whether to start observing immediately on mount. Default: true */
13
+ immediate?: boolean;
14
+ /** The element or document used as the viewport. Default: browser viewport */
15
+ root?: MaybeElement;
16
+ /** Margin around the root. Accepts CSS-style values. Default: "0px" */
17
+ rootMargin?: string;
18
+ /** Threshold(s) at which to trigger the callback. Default: 0 */
19
+ threshold?: number | number[];
20
+ }
21
+
22
+ export interface UseIntersectionObserverReturn {
23
+ isSupported: Observable<boolean>;
24
+ isActive: Observable<boolean>;
25
+ stop: () => void;
26
+ pause: () => void;
27
+ resume: () => void;
28
+ }
29
+
30
+ /**
31
+ * Reactive wrapper around the IntersectionObserver API.
32
+ * Observes one or more elements for intersection changes with pause/resume/stop support.
33
+ *
34
+ * @param target - Element(s) to observe: Ref$, Observable, raw Element, or array of these
35
+ * @param callback - Called when intersection state changes
36
+ * @param options - IntersectionObserver options plus an `immediate` flag
37
+ * @returns `{ isSupported, isActive, pause, resume, stop }`
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * const el$ = useRef$<HTMLDivElement>();
42
+ * const { isActive, pause, resume } = useIntersectionObserver(
43
+ * el$,
44
+ * (entries) => {
45
+ * entries.forEach(entry => console.log(entry.isIntersecting));
46
+ * },
47
+ * { threshold: 0.5 },
48
+ * );
49
+ * return <div ref={el$} />;
50
+ * ```
51
+ */
52
+ export function useIntersectionObserver(
53
+ target: MaybeElement | MaybeElement[],
54
+ callback: IntersectionObserverCallback,
55
+ options?: DeepMaybeObservable<UseIntersectionObserverOptions>,
56
+ ): UseIntersectionObserverReturn {
57
+ const opts$ = useMayObservableOptions<UseIntersectionObserverOptions>(
58
+ options,
59
+ {
60
+ immediate: "peek",
61
+ threshold: "peek",
62
+ root: "get.element",
63
+ rootMargin: (value) => get(value as MaybeObservable<string | undefined>),
64
+ },
65
+ );
66
+ const isSupported$ = useObservable<boolean>(
67
+ typeof IntersectionObserver !== "undefined",
68
+ );
69
+ const isActive$ = useObservable<boolean>(opts$.immediate.peek() !== false);
70
+ const observerRef = useRef<IntersectionObserver | null>(null);
71
+ const stoppedRef = useRef(false);
72
+ const mountedRef = useRef(false);
73
+
74
+ const cleanup = () => {
75
+ observerRef.current?.disconnect();
76
+ observerRef.current = null;
77
+ };
78
+
79
+ const setup = () => {
80
+ if (!isSupported$.peek() || !isActive$.peek()) return;
81
+ cleanup();
82
+
83
+ const rawRoot = opts$.root.peek();
84
+ const root =
85
+ rawRoot == null
86
+ ? (rawRoot as null | undefined)
87
+ : (() => {
88
+ const el = (
89
+ rawRoot as unknown as { valueOf(): HTMLElement | Document }
90
+ ).valueOf();
91
+ return isWindow(el as unknown) ? null : el;
92
+ })();
93
+
94
+ observerRef.current = new IntersectionObserver(callback, {
95
+ root: root ?? undefined,
96
+ rootMargin: opts$.rootMargin.peek() as string | undefined,
97
+ threshold: (opts$.threshold.peek() as number | number[] | undefined) ?? 0,
98
+ });
99
+
100
+ const targets = normalizeTargets(target);
101
+ targets.forEach((el) => observerRef.current?.observe(el));
102
+ };
103
+
104
+ // eslint-disable-next-line react-hooks/exhaustive-deps
105
+ useEffect(() => {
106
+ mountedRef.current = true;
107
+ return () => {
108
+ mountedRef.current = false;
109
+ cleanup();
110
+ };
111
+ }, []);
112
+
113
+ useObserveEffect((e) => {
114
+ e.onCleanup = cleanup;
115
+ const root = opts$.root.get();
116
+ opts$.rootMargin.get();
117
+ isActive$.get();
118
+ normalizeTargets(target);
119
+ if (stoppedRef.current) return;
120
+ if (root === null) return;
121
+ setup();
122
+ });
123
+
124
+ const pause = () => {
125
+ if (!mountedRef.current) return;
126
+ cleanup();
127
+ isActive$.set(false);
128
+ };
129
+
130
+ const resume = () => {
131
+ if (stoppedRef.current || !mountedRef.current) return;
132
+ isActive$.set(true);
133
+ };
134
+
135
+ const stop = () => {
136
+ if (!mountedRef.current) return;
137
+ stoppedRef.current = true;
138
+ cleanup();
139
+ isActive$.set(false);
140
+ };
141
+
142
+ return {
143
+ isSupported: isSupported$,
144
+ isActive: isActive$,
145
+ stop,
146
+ pause,
147
+ resume,
148
+ };
149
+ }